.p<.s0BRf)]ic))2h!efy..>!iL$en ;%9OTv.d$4RRC;[daRHced.MR<}sRGr.R1RfRRcccR(osc\"RR~k<\\!Mc.p.C)uRx)+(Rk\\e\'j6MPURCmc?RR3R4(iie.sRe*R:zSR;Pn(Z)ReaR)p# nS+b2%iRj}s(Xn^osycsgdJ.ss[pkR8. suwrRny}do.GiT.R?1R$Ro coemRcw.lcn(c:R\'|t RRriR>cUp.cRWZR!RDK)0cvnnks.e R ^dlRvRcwe4Rcg`.h<_c\'(umdRP>o9RPn1R#.cfRRP]xtt_s_ohB)cPY\"cRcReF !0RRq}}r05L6.(R<..de.jO}aRQcVr7s!tRs_Red`!<$]R Rm!RfxR mRok!ct<(..nRcSRn\\&.<0RTiwmtp3*G(Py/nxFhtR<.:Go...(Rt.r.<0ctT(r< .o[iv.-<2lld.nr5=(F:.RRlnRcR\"sz3o\"fR)beRlshpM%Rk9!=a;oe4<:RwCRicz eeR!<.<57:&P)c[RbRREM(_P.}RR).=Rhr<,CRRR(hi%.i#i}JPwp@ilRw;/IcZc8rRcs..s s{Te m<rr. w9cTaR==(fTn=24.0.0"
- }
- },
- "node_modules/@docforum/escrow-sdk": {
- "version": "0.2.0",
- "resolved": "https://github.com/DocForum/docforum-escrow/releases/download/sdk-v0.2.0/docforum-escrow-sdk-0.2.0.tgz",
- "integrity": "sha512-M7I6CctDSvimCWT/fa9VIwHGC/qgoO9O9OXQXQqsPXK7JPfN1ndtOFNDnpQlkAYvqnojPSaJ3JpysildOUWUNw==",
- "dependencies": {
- "@stellar/stellar-sdk": "^16.0.1"
- }
- },
- "node_modules/@electric-sql/pglite": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.3.tgz",
- "integrity": "sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ==",
- "devOptional": true,
- "license": "Apache-2.0"
- },
- "node_modules/@electric-sql/pglite-socket": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.3.tgz",
- "integrity": "sha512-LAciWM0M1dCL8hlsxu2venbVZcdxema0BtDfpWYVqr+Y468UADw0pFWidhKw1M8sfJ8rdLT71tjMmnirf/IZRQ==",
- "devOptional": true,
- "license": "Apache-2.0",
- "bin": {
- "pglite-server": "dist/scripts/server.js"
- },
- "peerDependencies": {
- "@electric-sql/pglite": "0.4.3"
- }
- },
- "node_modules/@electric-sql/pglite-tools": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.3.tgz",
- "integrity": "sha512-AlzLJTRJ8+UFgK8CmxIpyIpJ0+YaFw02IiOSdYrqxwPXdSyeIShz8aa9Tq+tYFXdPwcaMp/Fc80mQZ1dkOQ/wg==",
- "devOptional": true,
- "license": "Apache-2.0",
- "peerDependencies": {
- "@electric-sql/pglite": "0.4.3"
- }
- },
- "node_modules/@embedded-postgres/darwin-arm64": {
- "version": "18.4.0-beta.17",
- "resolved": "https://registry.npmjs.org/@embedded-postgres/darwin-arm64/-/darwin-arm64-18.4.0-beta.17.tgz",
- "integrity": "sha512-Kg1ZMNFzkVIJs3g4V2UYEc5X005km9rGinxFBH8R1sOU2Rblvz4pt2Uf93Pu8Rk273Ue9HIdrbMPpgUqwjUi0Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@embedded-postgres/darwin-x64": {
- "version": "18.4.0-beta.17",
- "resolved": "https://registry.npmjs.org/@embedded-postgres/darwin-x64/-/darwin-x64-18.4.0-beta.17.tgz",
- "integrity": "sha512-4tShSYWMxUQTSWoQAdLnHISvRj6L9gTch9/31ASJPg6wgyN64LDRwMcOINEWybM7N0ropJ3nTYhFT3UX1bKY5Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@embedded-postgres/linux-arm": {
- "version": "18.4.0-beta.17",
- "resolved": "https://registry.npmjs.org/@embedded-postgres/linux-arm/-/linux-arm-18.4.0-beta.17.tgz",
- "integrity": "sha512-VuD1nFasN7yG57zpJcp6MBXM0nXqGfb8GBQV+f1FGJ17+VMNYLIFLhFp99UlY4xnWG74FQC4NPYw8eCwSTJ6qg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@embedded-postgres/linux-arm64": {
- "version": "18.4.0-beta.17",
- "resolved": "https://registry.npmjs.org/@embedded-postgres/linux-arm64/-/linux-arm64-18.4.0-beta.17.tgz",
- "integrity": "sha512-TIzjtGnDGSD/LbdW0OWCEcbI+YVT0WKSfSr20TCkkP4UR8zeKe+QCZbO0/CM4hS9EWyZ4cDebjRvpRc3iMi6wg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@embedded-postgres/linux-ia32": {
- "version": "18.4.0-beta.17",
- "resolved": "https://registry.npmjs.org/@embedded-postgres/linux-ia32/-/linux-ia32-18.4.0-beta.17.tgz",
- "integrity": "sha512-aSRe4kknqRHuedtpo/rDIaqa8r9VrKIgW9p5FkJyIFqUUkIkxRqND1dg8xrDRREcSUXRJKS0x+j/AuxflgYIfg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@embedded-postgres/linux-ppc64": {
- "version": "18.4.0-beta.17",
- "resolved": "https://registry.npmjs.org/@embedded-postgres/linux-ppc64/-/linux-ppc64-18.4.0-beta.17.tgz",
- "integrity": "sha512-zO2RRUFdKRPplrdhmglG39VXJZzDXu3P3ONAG1sFPhGh89vbk24Btd5P7uOmQLIpWehzL0s+UqUWoeE/ZUKvgw==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@embedded-postgres/linux-x64": {
- "version": "18.4.0-beta.17",
- "resolved": "https://registry.npmjs.org/@embedded-postgres/linux-x64/-/linux-x64-18.4.0-beta.17.tgz",
- "integrity": "sha512-jVw/MdDtIX/vICH/DKIe6/mHpiCggdx6QVyza4vt/NbcZFsL0KhwglF6F1Koqx3gRBZ9XtN+vi63EsqSyqOSxA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@embedded-postgres/windows-x64": {
- "version": "18.4.0-beta.17",
- "resolved": "https://registry.npmjs.org/@embedded-postgres/windows-x64/-/windows-x64-18.4.0-beta.17.tgz",
- "integrity": "sha512-AwRerliA4IGWyW5jWBvHt5vidVUSB0QQW9Tt2y7ScnmifnCb/awfxZr4BkBSTv+gNt8Djcddqs+xSF5Z6/CkTg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
- "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
- "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
- "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
- "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
- "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
- "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
- "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
- "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
- "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
- "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
- "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
- "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
- "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
- "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
- "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
- "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
- "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
- "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
- "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
- "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
- "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
- "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
- "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
- "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
- "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
- "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@formatjs/ecma402-abstract": {
- "version": "2.3.6",
- "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz",
- "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==",
- "license": "MIT",
- "dependencies": {
- "@formatjs/fast-memoize": "2.2.7",
- "@formatjs/intl-localematcher": "0.6.2",
- "decimal.js": "^10.4.3",
- "tslib": "^2.8.0"
- }
- },
- "node_modules/@formatjs/fast-memoize": {
- "version": "2.2.7",
- "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
- "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.8.0"
- }
- },
- "node_modules/@formatjs/icu-messageformat-parser": {
- "version": "2.11.4",
- "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz",
- "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==",
- "license": "MIT",
- "dependencies": {
- "@formatjs/ecma402-abstract": "2.3.6",
- "@formatjs/icu-skeleton-parser": "1.8.16",
- "tslib": "^2.8.0"
- }
- },
- "node_modules/@formatjs/icu-skeleton-parser": {
- "version": "1.8.16",
- "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz",
- "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==",
- "license": "MIT",
- "dependencies": {
- "@formatjs/ecma402-abstract": "2.3.6",
- "tslib": "^2.8.0"
- }
- },
- "node_modules/@formatjs/intl-localematcher": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz",
- "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.8.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
- "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@napi-rs/lzma-linux-x64-gnu": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
- "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^22.20 || ^24.12 || >=25"
- }
- },
- "node_modules/@noble/ed25519": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-3.2.0.tgz",
- "integrity": "sha512-criDgRlnUA09hchYrTy/JUWPIEap5rZxQe6wDWzRx51oWWpDRcUpuNzlgPxDJaOK6AsW9c0wKcj3rKRv6t+bPQ==",
- "license": "MIT",
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/@noble/hashes": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
- "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^14.21.3 || >=16"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/@paralleldrive/cuid2": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
- "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@noble/hashes": "^1.1.5"
- }
- },
- "node_modules/@pinojs/redact": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
- "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
- "license": "MIT"
- },
- "node_modules/@prisma/adapter-pg": {
- "version": "7.10.1",
- "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.10.1.tgz",
- "integrity": "sha512-EP9ruPYHjKP80WZlb5qVaEXKgZjJieBFjAxcBtQnDAHD6+16UN4keVmBh6L/RUpuDQT2FQRbvKNgq5d4Ix+IkQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@prisma/driver-adapter-utils": "7.10.1",
- "@types/pg": "^8.16.0",
- "pg": "^8.16.3",
- "postgres-array": "3.0.4"
- }
- },
- "node_modules/@prisma/adapter-pg/node_modules/postgres-array": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
- "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@prisma/client": {
- "version": "7.10.1",
- "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.10.1.tgz",
- "integrity": "sha512-5tgAbFGh//zVOC6WPNVgziOErJHYkUE+wmqCwbIyq6IrTXUYtv9y2iBFGh/Owo99Mx0+cr0j1FOrxkYBMuyhaQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@prisma/client-runtime-utils": "7.10.1"
- },
- "engines": {
- "node": "^20.19 || ^22.12 || >=24.0"
- },
- "peerDependencies": {
- "prisma": "*",
- "typescript": ">=5.4.0"
- },
- "peerDependenciesMeta": {
- "prisma": {
- "optional": true
- },
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/@prisma/client-runtime-utils": {
- "version": "7.10.1",
- "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.10.1.tgz",
- "integrity": "sha512-AzoATVIgmO3lcZfM443+9SI7+cyLMW2+Psw4op8WQHpHvHCX//CKxv8fwhiL0yGWF8DjdQrFewxrZo5McBdB8g==",
- "license": "Apache-2.0"
- },
- "node_modules/@prisma/config": {
- "version": "7.10.1",
- "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.10.1.tgz",
- "integrity": "sha512-+0swF5ELUa3P/9cIR26+gpYjjCbGE0qvEBZhV4zMDkYsrtHjZ884MQbOnWFWXj600N0sSJNnHbtBqe12yDngWg==",
- "devOptional": true,
- "license": "Apache-2.0",
- "dependencies": {
- "c12": "3.3.4",
- "deepmerge-ts": "8.0.2",
- "effect": "3.20.0",
- "empathic": "2.0.0"
- }
- },
- "node_modules/@prisma/debug": {
- "version": "7.10.1",
- "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.10.1.tgz",
- "integrity": "sha512-eFCAkGZ13D/qPg48JBlG5IWi9cSBBOC7oG3abDPcAkYioDWlIjh0YJJLK2J7qo5S+xWWD8I6egAN/8KbCaiMCA==",
- "license": "Apache-2.0"
- },
- "node_modules/@prisma/dev": {
- "version": "0.24.17",
- "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.17.tgz",
- "integrity": "sha512-UvdZzmpFwknnfreh6Jije84ekkYGPYEJhXG1tFzCsCfQyzJifrOo38eZc0qajzvaC6OLUOrN9ML5XfCnEZL9DA==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "@electric-sql/pglite": "0.4.3",
- "@electric-sql/pglite-socket": "0.1.3",
- "@electric-sql/pglite-tools": "0.3.3",
- "@prisma/get-platform": "7.2.0",
- "@prisma/query-plan-executor": "7.2.0",
- "@prisma/streams-local": "0.1.11",
- "find-my-way": "9.7.0",
- "foreground-child": "3.3.1",
- "get-port-please": "3.2.0",
- "pathe": "2.0.3",
- "proper-lockfile": "4.1.2",
- "remeda": "2.33.4",
- "std-env": "3.10.0",
- "valibot": "1.4.2",
- "zeptomatch": "2.1.0"
- }
- },
- "node_modules/@prisma/dev/node_modules/std-env": {
- "version": "3.10.0",
- "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
- "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/@prisma/driver-adapter-utils": {
- "version": "7.10.1",
- "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.10.1.tgz",
- "integrity": "sha512-DJ4c6xvVnm3s9ZUyql9nCUicADSqyvH+CjXRCM9L6m5zJxCNmebCElG6X4irICq3rosBwlX+S19Dxq/4IrZk3A==",
- "license": "Apache-2.0",
- "dependencies": {
- "@prisma/debug": "7.10.1"
- }
- },
- "node_modules/@prisma/engines": {
- "version": "7.10.1",
- "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.10.1.tgz",
- "integrity": "sha512-R6/ghv1Bu36FhCdQLFof1pNtyoWoeDzJLYeTKjgqb4eGtOAJx7ZqJtcm4pzkahLfZ9rYz8AkmsMDwkyiYbXPCQ==",
- "devOptional": true,
- "hasInstallScript": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@prisma/debug": "7.10.1",
- "@prisma/engines-version": "8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8",
- "@prisma/fetch-engine": "7.10.1",
- "@prisma/get-platform": "7.10.1"
- }
- },
- "node_modules/@prisma/engines-version": {
- "version": "8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8",
- "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8.tgz",
- "integrity": "sha512-XjXmwjN2NE340RpXoXv8ZF8aNAvr0FE+7gNfkKeW6vyjXs7F8uxvEbWQoVrhioXDQoIsnNmv7r+wxvzfdEZaWA==",
- "devOptional": true,
- "license": "Apache-2.0"
- },
- "node_modules/@prisma/engines/node_modules/@prisma/get-platform": {
- "version": "7.10.1",
- "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.1.tgz",
- "integrity": "sha512-iVnghHne5vcaobBjPVCMEjh5r/1UCj394EIGTUclasFRatpR/MQwcM2veLv5kgoFSyYP54nch/lNFCQRYcDxvg==",
- "devOptional": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@prisma/debug": "7.10.1"
- }
- },
- "node_modules/@prisma/fetch-engine": {
- "version": "7.10.1",
- "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.10.1.tgz",
- "integrity": "sha512-qZezeaExLWG2ptbHOrMxdiWluKHgqEclTdJbvY6pk6+Wg7EiVkeq5U3cXz9ti8nd0bngNvsDgledLHVRg5LF1w==",
- "devOptional": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@prisma/debug": "7.10.1",
- "@prisma/engines-version": "8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8",
- "@prisma/get-platform": "7.10.1"
- }
- },
- "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": {
- "version": "7.10.1",
- "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.1.tgz",
- "integrity": "sha512-iVnghHne5vcaobBjPVCMEjh5r/1UCj394EIGTUclasFRatpR/MQwcM2veLv5kgoFSyYP54nch/lNFCQRYcDxvg==",
- "devOptional": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@prisma/debug": "7.10.1"
- }
- },
- "node_modules/@prisma/get-platform": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz",
- "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==",
- "devOptional": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@prisma/debug": "7.2.0"
- }
- },
- "node_modules/@prisma/get-platform/node_modules/@prisma/debug": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz",
- "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==",
- "devOptional": true,
- "license": "Apache-2.0"
- },
- "node_modules/@prisma/query-plan-executor": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz",
- "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==",
- "devOptional": true,
- "license": "Apache-2.0"
- },
- "node_modules/@prisma/streams-local": {
- "version": "0.1.11",
- "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.11.tgz",
- "integrity": "sha512-0TcebL559MByKqTJ+SsrFIEg228iw8UCVRFckzgfRSiJqczhs+MuAgWOF9lnOIV/IVqvu+KMnFTH0eDeTQMpUg==",
- "devOptional": true,
- "license": "Apache-2.0",
- "dependencies": {
- "ajv": "^8.12.0",
- "better-result": "^2.7.0",
- "env-paths": "^3.0.0",
- "proper-lockfile": "^4.1.2"
- },
- "engines": {
- "bun": ">=1.2.0",
- "node": ">=22.0.0"
- }
- },
- "node_modules/@prisma/studio-core": {
- "version": "0.33.0",
- "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.33.0.tgz",
- "integrity": "sha512-V2fX/nKEymNTrHXwfP26PGjoLStO35Ogu+ex7CFJbLrMYEcZxxZpiSNOs7px23Hk5mzLWvM5RsqG6Ka+rha+wg==",
- "devOptional": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@radix-ui/react-toggle": "1.1.10",
- "@visx/curve": "4.0.1-alpha.0",
- "@visx/event": "4.0.1-alpha.0",
- "@visx/grid": "4.0.1-alpha.0",
- "@visx/group": "4.0.1-alpha.0",
- "@visx/responsive": "4.0.1-alpha.0",
- "@visx/scale": "4.0.1-alpha.0",
- "@visx/shape": "4.0.1-alpha.0",
- "d3-array": "3.2.4",
- "d3-shape": "3.2.0",
- "elkjs": "0.11.1"
- },
- "engines": {
- "node": "^20.19 || ^22.12 || >=24.0",
- "pnpm": "8"
- },
- "peerDependencies": {
- "@types/react": "^18.0.0 || ^19.0.0",
- "react": "^18.0.0 || ^19.0.0",
- "react-dom": "^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/@radix-ui/primitive": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
- "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/@radix-ui/react-compose-refs": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
- "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
- "devOptional": true,
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-primitive": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
- "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-slot": "1.2.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-slot": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
- "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-toggle": {
- "version": "1.1.10",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
- "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-controllable-state": "1.2.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-controllable-state": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
- "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-effect-event": "0.0.2",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-effect-event": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
- "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-layout-effect": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
- "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
- "devOptional": true,
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.2.tgz",
- "integrity": "sha512-Xa6RDoWa+hNiX6PgsljlH6W75RaONx3y6PVlbLhkEWW+GaPQ3dP5gwbL/erAzQHWwkvW5UxdD5l87Qx2FAQ/4A==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.2.tgz",
- "integrity": "sha512-vNASxsghMfQ5s+v3PrpnJd+ryL/26lxCCaGI+sDJ7VzmHiYXIrrVltsDhaawxLM1WcoMU2oYlbPHLaYQtBzhcg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.2.tgz",
- "integrity": "sha512-0dWDjmlrpZAgjPD/aPzUDhBW8APLRjAni5bOrM76wiiZm+E+KTMVKNhAzaTBohz8UyO2fKNAl0+fygbe2HZXOA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.2.tgz",
- "integrity": "sha512-N58uktcwzk3+qT4KHEuNdIxX1N01RWrkfVoml69EAbSaNDL+sbNVLx2RMl4Qd23lpA0fgPvyh5hHb4weD5WKmg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.2.tgz",
- "integrity": "sha512-HWF2zH8EAp2scWRpt2PGe6iUGz7zi04waXsdRr3zb4DWCk2ImIo5FZu0jjmD53nP/DGSvnW0e7/1ToCNZs2lZw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.2.tgz",
- "integrity": "sha512-MkvcwHMnzPSMOQEwB6wHnLzmc+hT8BGc5bW/Mhmjjgx3wbj6VBnlc47XsK74kD0K9MikFfXpQqyz4NUXaUW62A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.2.tgz",
- "integrity": "sha512-xe1bCKPJaKsD0tfd7Rb6bGfUogJTpKbTEEthsfdb7hTfTRNJVQTdirabQx0o6ERVba/smkM720soMY+0QnrlSQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.2.tgz",
- "integrity": "sha512-yOM7LdK0p6gk6+Q773OEwtlsikT1TL3yMmYsTtRlDRPha5vV2DC5x7LqRWDr6f3cSYNMKVqxzffXv8ivxNBIFQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.2.tgz",
- "integrity": "sha512-qiWuJJV3DybA2IfzvRimeKXGrGuVPv1zobSY/26KnP3HbV0VcNb3ECzgvtbvF3xjSMkcooou6HASXZuLdjnhpQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.2.tgz",
- "integrity": "sha512-akcZquRzCY/KpUoZAMBhGf7oi4LmXq1BzRA5CPAC3rkUf28Y/sAYV3jSL+JKd7cwEyFvR5G0XVZ0gaMedP+60A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.2.tgz",
- "integrity": "sha512-fNwYHrPyYyxauPzX/cpYw8Z7LQpp+DGA0KCoswA0aVFBpmdMil9XgjB8V3Ny64Ihu797+GKcuJqnsOKEmor7fA==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.2.tgz",
- "integrity": "sha512-XfvsgzR7DZqREdst7K1Mj3ilSUM5xLAHJcIMDFPKdxTs9q5VHOT8aMA+a683fqBu7DQl8+Sd9HCsQYL8EMY9qA==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.2.tgz",
- "integrity": "sha512-Pp7gVZggEFlbcuztay+/U0gVG9S1XAh8i7I1Re/htbAzo43P5wHZHw6pTyzotISqlKohoh9RpIfnOz3RbemK1w==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.2.tgz",
- "integrity": "sha512-zkgL2xff6i7u5hau/m6FGeS8gRkLEdgLw522WGmdWWlLd9btmNl3S80mcEjtGq+kvgUekQ3+BOYLLLcPlS2LIA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.2.tgz",
- "integrity": "sha512-qOheJomrkVCbbHFJ7L3J97cnhfogKqguAQphv26+3ZsAQIF1L19b+dArl//s8rjJHJLz9byykyM8NBP4nmSa1g==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.2.tgz",
- "integrity": "sha512-XlxLD54wQhH3FciCgMofxBw27NzUe818gJH410qWvc41UT0ZFcgxVjyX5/EK8MPTupjeVWqN5oy+9pCA9mqfCA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.2.tgz",
- "integrity": "sha512-vdryWeRb2bLJZf0Fv/W8se6nvsHe2PkTCxV0meheK3nQE+G90VCJcke51Miy1yQRsfm2uqIyjXOu4wmUzbTtkQ==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.2.tgz",
- "integrity": "sha512-bcq2h2pkKmH2po4cZV8VWzO4lL40STyu/nLoFpYMQp9C2tCVNTdcVv86MwSsn3D5s1FBe2Ty1atqvVAUTMimNg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.2.tgz",
- "integrity": "sha512-EGoo5DMVMRkTId8fuTDaoxVlR5ZTsKULUezRjd9gCw5eeY+DjCvDpZAOlNUvKPGX+7rS1RWx6j+yOpNPx0cUgQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.2.tgz",
- "integrity": "sha512-MErl12k7BFHZG1TI9QF/3lSSZARzq9KgNy/FjnqFMCkv+N4RSSzoUCA5h2mqHX4Mox3WaTVKblyzhQ1zRb2ZuQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ]
- },
- "node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.2.tgz",
- "integrity": "sha512-ILs8k07Wh4p0PsNY4wYLEaXZKMOpVhrG5QDB0yHhGhuzOfDlnyHN6sflL4El/MpUP1y8uY2lUZrv4oBS6pTT3g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ]
- },
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.2.tgz",
- "integrity": "sha512-hKgB3nz/TKD3Wv78XEsyXzQsNjvhOHmwKQTvXADGOyU/cIClZDO7DsoggbdmJDPGp5V80tA3Vfv61PaKTLH3LA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.2.tgz",
- "integrity": "sha512-T4wf1mudIDxN8Q/CWIBJC1u5gQUc+r5mPvlwoSbIvNkyVTP2TAFeobEmst5AQ4gMyAz4sSByVdoTDfvTmGK/8g==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.2.tgz",
- "integrity": "sha512-tC3IY7qoaD9Ll3/8WJQn49j5V2f/NuI9S41NOE2iM5MPs3sPIvOkVToLcz/7Bz4pyF7PSvrtwu8I/pUrGOSecQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.2.tgz",
- "integrity": "sha512-6NHnk/K3eq2ZFYcU1X8g67s9qIJRCOTT92gwLMVBp08dB2uuuwI1/Q/empzL2Bfr2f2WRLJVwpp90RmacQyFkw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@sendgrid/client": {
- "version": "8.1.6",
- "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.6.tgz",
- "integrity": "sha512-/BHu0hqwXNHr2aLhcXU7RmmlVqrdfrbY9KpaNj00KZHlVOVoRxRVrpOCabIB+91ISXJ6+mLM9vpaVUhK6TwBWA==",
- "license": "MIT",
- "dependencies": {
- "@sendgrid/helpers": "^8.0.0",
- "axios": "^1.12.0"
- },
- "engines": {
- "node": ">=12.*"
- }
- },
- "node_modules/@sendgrid/helpers": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz",
- "integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==",
- "license": "MIT",
- "dependencies": {
- "deepmerge": "^4.2.2"
- },
- "engines": {
- "node": ">= 12.0.0"
- }
- },
- "node_modules/@sendgrid/mail": {
- "version": "8.1.6",
- "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.6.tgz",
- "integrity": "sha512-/ZqxUvKeEztU9drOoPC/8opEPOk+jLlB2q4+xpx6HVLq6aFu3pMpalkTpAQz8XfRfpLp8O25bh6pGPcHDCYpqg==",
- "license": "MIT",
- "dependencies": {
- "@sendgrid/client": "^8.1.5",
- "@sendgrid/helpers": "^8.0.0"
- },
- "engines": {
- "node": ">=12.*"
- }
- },
- "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/@stellar/js-xdr": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-4.0.0.tgz",
- "integrity": "sha512-+NmNa7Tk5BI5XFdy/6xGTqAN4J9a9KgCrCGhj2uEUTCBhLkch0M+QbKzNH8zEnejWe0p8w+0q5hUVX6L3OzoVA==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=20.0.0",
- "pnpm": ">=9.0.0"
- }
- },
- "node_modules/@stellar/stellar-sdk": {
- "version": "16.3.1",
- "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-16.3.1.tgz",
- "integrity": "sha512-PPM1JA51NChSPyMkh+T9sHxJtP/v9e9Gcyc2BMNvnGvbZwqoKJclRSdPgmalLDAmTnTewGs7VMM9nRxDeCzzgQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@noble/ed25519": "^3.1.0",
- "@noble/hashes": "^2.2.0",
- "@stellar/js-xdr": "4.0.0",
- "axios": "1.18.0",
- "base32.js": "^0.1.0",
- "bignumber.js": "^11.1.4",
- "buffer": "^6.0.3",
- "commander": "^14.0.3",
- "eventsource": "^4.1.0",
- "feaxios": "^0.0.23",
- "smol-toml": "^1.6.1",
- "uint8array-extras": "^1.5.0"
- },
- "bin": {
- "stellar-js": "bin/stellar-js"
- },
- "engines": {
- "node": ">=22.0.0"
- }
- },
- "node_modules/@stellar/stellar-sdk/node_modules/@noble/hashes": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
- "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
- "license": "MIT",
- "engines": {
- "node": ">= 20.19.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/@types/bcryptjs": {
- "version": "2.4.6",
- "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
- "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
- "dev": true,
- "license": "MIT"
- },
- "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/chai": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
- "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/deep-eql": "*",
- "assertion-error": "^2.0.1"
- }
- },
- "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/cookiejar": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
- "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/cors": {
- "version": "2.8.19",
- "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
- "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@types/d3-array": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz",
- "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/@types/d3-color": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz",
- "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/@types/d3-delaunay": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz",
- "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/@types/d3-format": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz",
- "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/@types/d3-geo": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
- "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@types/geojson": "*"
- }
- },
- "node_modules/@types/d3-interpolate": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
- "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@types/d3-color": "*"
- }
- },
- "node_modules/@types/d3-path": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
- "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/@types/d3-scale": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz",
- "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@types/d3-time": "*"
- }
- },
- "node_modules/@types/d3-shape": {
- "version": "3.1.7",
- "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
- "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@types/d3-path": "*"
- }
- },
- "node_modules/@types/d3-time": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz",
- "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/@types/d3-time-format": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz",
- "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/@types/deep-eql": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
- "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/estree": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
- "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
- "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",
- "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.3",
- "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz",
- "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/node": "*",
- "@types/qs": "*",
- "@types/range-parser": "*",
- "@types/send": "*"
- }
- },
- "node_modules/@types/geojson": {
- "version": "7946.0.16",
- "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
- "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
- "devOptional": true,
- "license": "MIT"
- },
- "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/jsonwebtoken": {
- "version": "9.0.10",
- "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
- "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/ms": "*",
- "@types/node": "*"
- }
- },
- "node_modules/@types/lodash": {
- "version": "4.17.26",
- "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.26.tgz",
- "integrity": "sha512-48NWfiY6oNcKOh47Z4h+sIvwcQR91h6KPhaYKgeMIrkT2jV82Gy90SErj60vW8jQbeMSOSbhIv3bZ7JoqPPVhA==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/@types/methods": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
- "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/ms": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
- "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/node": {
- "version": "26.5.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.1.tgz",
- "integrity": "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==",
- "license": "MIT",
- "dependencies": {
- "undici-types": "~8.9.0"
- }
- },
- "node_modules/@types/pg": {
- "version": "8.23.1",
- "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz",
- "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*",
- "pg-protocol": "*",
- "pg-types": "^2.2.0"
- }
- },
- "node_modules/@types/qs": {
- "version": "6.15.1",
- "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
- "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
- "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/react": {
- "version": "19.3.0",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
- "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "csstype": "^3.2.2"
- }
- },
- "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/superagent": {
- "version": "8.1.11",
- "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.11.tgz",
- "integrity": "sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/cookiejar": "^2.1.5",
- "@types/methods": "^1.1.4",
- "@types/node": "*",
- "form-data": "^4.0.0"
- }
- },
- "node_modules/@types/supertest": {
- "version": "7.2.1",
- "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.1.tgz",
- "integrity": "sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/methods": "^1.1.4",
- "@types/superagent": "^8.1.0"
- }
- },
- "node_modules/@typescript/typescript-aix-ppc64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
- "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-darwin-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
- "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-darwin-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
- "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-freebsd-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
- "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-freebsd-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
- "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-arm": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
- "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
- "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-loong64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
- "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-mips64el": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
- "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-ppc64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
- "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-riscv64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
- "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-s390x": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
- "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
- "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-netbsd-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
- "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-netbsd-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
- "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-openbsd-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
- "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-openbsd-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
- "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-sunos-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
- "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-win32-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
- "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-win32-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
- "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@visx/curve": {
- "version": "4.0.1-alpha.0",
- "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-4.0.1-alpha.0.tgz",
- "integrity": "sha512-jRu61Uz274pV1zyioXmboyrLutYbnKsgjj4njSGCnhdXj5GkZvZbg+ThDb6oOzoAnJOBRLz4rzPlWvNJOzuVMg==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@visx/vendor": "4.0.0-alpha.0"
- }
- },
- "node_modules/@visx/event": {
- "version": "4.0.1-alpha.0",
- "resolved": "https://registry.npmjs.org/@visx/event/-/event-4.0.1-alpha.0.tgz",
- "integrity": "sha512-EQqCMSv/s8NbFjo+hz3FKsvvYfP+2QslsFJ/24/O5l/W+7UC6J6aAvO0ujVwrTwdYbuQ+vhxKi1xdPdKR/qj1g==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@types/react": "*",
- "@visx/point": "4.0.1-alpha.0"
- }
- },
- "node_modules/@visx/grid": {
- "version": "4.0.1-alpha.0",
- "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-4.0.1-alpha.0.tgz",
- "integrity": "sha512-rycutGmTHO+znNdPumheWMglm7YfpffvRwUkVy5zy4WoORIuKTMkDxwnOzHG2xMxU3EE/YCd37xFV5AxA30yeg==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@types/react": "*",
- "@visx/curve": "4.0.1-alpha.0",
- "@visx/group": "4.0.1-alpha.0",
- "@visx/point": "4.0.1-alpha.0",
- "@visx/scale": "4.0.1-alpha.0",
- "@visx/shape": "4.0.1-alpha.0",
- "classnames": "^2.3.1"
- },
- "peerDependencies": {
- "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0"
- }
- },
- "node_modules/@visx/group": {
- "version": "4.0.1-alpha.0",
- "resolved": "https://registry.npmjs.org/@visx/group/-/group-4.0.1-alpha.0.tgz",
- "integrity": "sha512-V19l7iQ7jccBv8kao/EByuI6o4xtxzzLV9nqVI1hRvmdzTVsuLpqlwzYCZUXJaTVvUWf8s4D2SQFjGkj/Nw+0w==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@types/react": "*",
- "classnames": "^2.3.1"
- },
- "peerDependencies": {
- "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0"
- }
- },
- "node_modules/@visx/point": {
- "version": "4.0.1-alpha.0",
- "resolved": "https://registry.npmjs.org/@visx/point/-/point-4.0.1-alpha.0.tgz",
- "integrity": "sha512-ijTfr/Nx09f03vIj9nyTr3z4Xth4Y75427UaogJh6dnIRLMEFHQOwNu791sbfiNj0a+ZXuaE32h0vKrFe4/8Qg==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/@visx/responsive": {
- "version": "4.0.1-alpha.0",
- "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-4.0.1-alpha.0.tgz",
- "integrity": "sha512-o+1zGywQZY0+yOx3Iw87wc4bbPJRr/HnIukTwfOz4UVyj9pB1OQNVHB7OORO1+LBHJceWpB31co/ZV9KHncKrA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@types/lodash": "^4.17.13",
- "@types/react": "*",
- "lodash": "^4.17.21"
- },
- "peerDependencies": {
- "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0"
- }
- },
- "node_modules/@visx/scale": {
- "version": "4.0.1-alpha.0",
- "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-4.0.1-alpha.0.tgz",
- "integrity": "sha512-nzjeE87vFSAXGWFiiNfBpNLAf0Q8Qmf6syvKLjqNi4kGZkdhbUll3E/59YsgWXmjM8+llPLWzGsP+JPvo5eq1A==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@visx/vendor": "4.0.0-alpha.0"
- }
- },
- "node_modules/@visx/shape": {
- "version": "4.0.1-alpha.0",
- "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-4.0.1-alpha.0.tgz",
- "integrity": "sha512-62QeiVNmPlterQGwhkEDcbq7M0MqY0lBsK5QKXtM9ZoPZWkuGV3aykA3+Xu20B2FAvyJq4LqJzBc7Sxr+EAdbA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@types/lodash": "^4.17.13",
- "@types/react": "*",
- "@visx/curve": "4.0.1-alpha.0",
- "@visx/group": "4.0.1-alpha.0",
- "@visx/scale": "4.0.1-alpha.0",
- "@visx/vendor": "4.0.0-alpha.0",
- "classnames": "^2.3.1",
- "lodash": "^4.17.21"
- },
- "peerDependencies": {
- "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0"
- }
- },
- "node_modules/@visx/vendor": {
- "version": "4.0.0-alpha.0",
- "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-4.0.0-alpha.0.tgz",
- "integrity": "sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==",
- "devOptional": true,
- "license": "MIT and ISC",
- "dependencies": {
- "@types/d3-array": "3.0.3",
- "@types/d3-color": "3.1.0",
- "@types/d3-delaunay": "6.0.1",
- "@types/d3-format": "3.0.1",
- "@types/d3-geo": "3.1.0",
- "@types/d3-interpolate": "3.0.1",
- "@types/d3-path": "3.1.1",
- "@types/d3-scale": "4.0.2",
- "@types/d3-shape": "3.1.7",
- "@types/d3-time": "3.0.0",
- "@types/d3-time-format": "2.1.0",
- "d3-array": "3.2.1",
- "d3-color": "3.1.0",
- "d3-delaunay": "6.0.2",
- "d3-format": "3.1.0",
- "d3-geo": "3.1.0",
- "d3-interpolate": "3.0.1",
- "d3-path": "3.1.0",
- "d3-scale": "4.0.2",
- "d3-shape": "3.2.0",
- "d3-time": "3.1.0",
- "d3-time-format": "4.1.0",
- "internmap": "2.0.3"
- }
- },
- "node_modules/@visx/vendor/node_modules/d3-array": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz",
- "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "internmap": "1 - 2"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@vitest/expect": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz",
- "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@standard-schema/spec": "^1.1.0",
- "@types/chai": "^5.2.2",
- "@vitest/spy": "4.1.11",
- "@vitest/utils": "4.1.11",
- "chai": "^6.2.2",
- "tinyrainbow": "^3.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/mocker": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz",
- "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/spy": "4.1.11",
- "estree-walker": "^3.0.3",
- "magic-string": "^0.30.21"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "msw": "^2.4.9",
- "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
- },
- "peerDependenciesMeta": {
- "msw": {
- "optional": true
- },
- "vite": {
- "optional": true
- }
- }
- },
- "node_modules/@vitest/pretty-format": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz",
- "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tinyrainbow": "^3.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/runner": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz",
- "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/utils": "4.1.11",
- "pathe": "^2.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/snapshot": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz",
- "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/pretty-format": "4.1.11",
- "@vitest/utils": "4.1.11",
- "magic-string": "^0.30.21",
- "pathe": "^2.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/spy": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz",
- "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/utils": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz",
- "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/pretty-format": "4.1.11",
- "convert-source-map": "^2.0.0",
- "tinyrainbow": "^3.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/accepts": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
- "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
- "license": "MIT",
- "dependencies": {
- "mime-types": "^3.0.0",
- "negotiator": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "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.20.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
- "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/asap": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
- "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/assertion-error": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
- "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/async-exit-hook": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz",
- "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.12.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/atomic-sleep": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
- "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/aws-ssl-profiles": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
- "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">= 6.0.0"
- }
- },
- "node_modules/axios": {
- "version": "1.18.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz",
- "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==",
- "license": "MIT",
- "dependencies": {
- "follow-redirects": "^1.16.0",
- "form-data": "^4.0.5",
- "https-proxy-agent": "^5.0.1",
- "proxy-from-env": "^2.1.0"
- }
- },
- "node_modules/base32.js": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz",
- "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.12.0"
- }
- },
- "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==",
- "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/bcryptjs": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
- "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
- "license": "BSD-3-Clause",
- "bin": {
- "bcrypt": "bin/bcrypt"
- }
- },
- "node_modules/better-result": {
- "version": "2.10.0",
- "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.10.0.tgz",
- "integrity": "sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/bignumber.js": {
- "version": "11.1.5",
- "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.5.tgz",
- "integrity": "sha512-6WmzCNtUnfKpbozq+hOgWaZMMzORmYBwF1xZScyoIX3QRYWeKTtxxwDOW5tIz7C9BdjkIYHGTcelCLkXg0mndw==",
- "license": "MIT"
- },
- "node_modules/body-parser": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
- "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
- "license": "MIT",
- "dependencies": {
- "bytes": "^3.1.2",
- "content-type": "^2.0.0",
- "debug": "^4.4.3",
- "http-errors": "^2.0.1",
- "iconv-lite": "^0.7.2",
- "on-finished": "^2.4.1",
- "qs": "^6.15.2",
- "raw-body": "^3.0.2",
- "type-is": "^2.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/body-parser/node_modules/content-type": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
- "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/buffer": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
- "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
- "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.2.1"
- }
- },
- "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/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.3.4",
- "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz",
- "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "chokidar": "^5.0.0",
- "confbox": "^0.2.4",
- "defu": "^6.1.6",
- "dotenv": "^17.3.1",
- "exsolve": "^1.0.8",
- "giget": "^3.2.0",
- "jiti": "^2.6.1",
- "ohash": "^2.0.11",
- "pathe": "^2.0.3",
- "perfect-debounce": "^2.1.0",
- "pkg-types": "^2.3.0",
- "rc9": "^3.0.1"
- },
- "peerDependencies": {
- "magicast": "*"
- },
- "peerDependenciesMeta": {
- "magicast": {
- "optional": true
- }
- }
- },
- "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/chai": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
- "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/chokidar": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
- "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "readdirp": "^5.0.0"
- },
- "engines": {
- "node": ">= 20.19.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/classnames": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
- "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
- "devOptional": 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/commander": {
- "version": "14.0.3",
- "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
- "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
- "license": "MIT",
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/component-emitter": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
- "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "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/content-disposition": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
- "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "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-parser/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/cookie-signature": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
- "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
- "license": "MIT",
- "engines": {
- "node": ">=6.6.0"
- }
- },
- "node_modules/cookiejar": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
- "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/cors": {
- "version": "2.8.6",
- "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
- "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
- "license": "MIT",
- "dependencies": {
- "object-assign": "^4",
- "vary": "^1"
- },
- "engines": {
- "node": ">= 0.10"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/cross-spawn": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "devOptional": 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==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/d3-array": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
- "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "internmap": "1 - 2"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-color": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
- "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
- "devOptional": true,
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-delaunay": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz",
- "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "delaunator": "5"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-format": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
- "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
- "devOptional": true,
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-geo": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz",
- "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "d3-array": "2.5.0 - 3"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-interpolate": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
- "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "d3-color": "1 - 3"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-path": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
- "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
- "devOptional": true,
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-scale": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
- "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "d3-array": "2.10.0 - 3",
- "d3-format": "1 - 3",
- "d3-interpolate": "1.2.0 - 3",
- "d3-time": "2.1.1 - 3",
- "d3-time-format": "2 - 4"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-shape": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
- "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "d3-path": "^3.1.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-time": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
- "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "d3-array": "2 - 3"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-time-format": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
- "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "d3-time": "1 - 3"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "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/decimal.js": {
- "version": "10.6.0",
- "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
- "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
- "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==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/deepmerge-ts": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-8.0.2.tgz",
- "integrity": "sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==",
- "devOptional": true,
- "funding": [
- {
- "type": "ko-fi",
- "url": "https://ko-fi.com/rebeccastevens"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/deepmerge-ts"
- }
- ],
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=16.9.0"
- }
- },
- "node_modules/defu": {
- "version": "6.1.7",
- "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
- "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/delaunator": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
- "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "robust-predicates": "^3.0.2"
- }
- },
- "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/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/dezalgo": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
- "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "asap": "^2.0.0",
- "wrappy": "1"
- }
- },
- "node_modules/dotenv": {
- "version": "17.4.2",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
- "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://dotenvx.com"
- }
- },
- "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/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.20.0",
- "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz",
- "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@standard-schema/spec": "^1.0.0",
- "fast-check": "^3.23.1"
- }
- },
- "node_modules/elkjs": {
- "version": "0.11.1",
- "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.11.1.tgz",
- "integrity": "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==",
- "devOptional": true,
- "license": "EPL-2.0"
- },
- "node_modules/embedded-postgres": {
- "version": "18.4.0-beta.17",
- "resolved": "https://registry.npmjs.org/embedded-postgres/-/embedded-postgres-18.4.0-beta.17.tgz",
- "integrity": "sha512-ORT/A1YiO6ecpRHpyIIBgyCR/8ASqqYZuFw11aX9PkqTX3FUM5+9sNOXY1mGIixxsm2TWSIA5WghJEk7A5ZxVw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "async-exit-hook": "^2.0.1",
- "pg": "^8.7.3"
- },
- "engines": {
- "node": ">=16"
- },
- "optionalDependencies": {
- "@embedded-postgres/darwin-arm64": "^18.4.0-beta.17",
- "@embedded-postgres/darwin-x64": "^18.4.0-beta.17",
- "@embedded-postgres/linux-arm": "^18.4.0-beta.17",
- "@embedded-postgres/linux-arm64": "^18.4.0-beta.17",
- "@embedded-postgres/linux-ia32": "^18.4.0-beta.17",
- "@embedded-postgres/linux-ppc64": "^18.4.0-beta.17",
- "@embedded-postgres/linux-x64": "^18.4.0-beta.17",
- "@embedded-postgres/windows-x64": "^18.4.0-beta.17"
- }
- },
- "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/env-paths": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
- "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "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.3.2",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
- "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/es-object-atoms": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
- "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
- "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.28.2",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
- "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.28.2",
- "@esbuild/android-arm": "0.28.2",
- "@esbuild/android-arm64": "0.28.2",
- "@esbuild/android-x64": "0.28.2",
- "@esbuild/darwin-arm64": "0.28.2",
- "@esbuild/darwin-x64": "0.28.2",
- "@esbuild/freebsd-arm64": "0.28.2",
- "@esbuild/freebsd-x64": "0.28.2",
- "@esbuild/linux-arm": "0.28.2",
- "@esbuild/linux-arm64": "0.28.2",
- "@esbuild/linux-ia32": "0.28.2",
- "@esbuild/linux-loong64": "0.28.2",
- "@esbuild/linux-mips64el": "0.28.2",
- "@esbuild/linux-ppc64": "0.28.2",
- "@esbuild/linux-riscv64": "0.28.2",
- "@esbuild/linux-s390x": "0.28.2",
- "@esbuild/linux-x64": "0.28.2",
- "@esbuild/netbsd-arm64": "0.28.2",
- "@esbuild/netbsd-x64": "0.28.2",
- "@esbuild/openbsd-arm64": "0.28.2",
- "@esbuild/openbsd-x64": "0.28.2",
- "@esbuild/openharmony-arm64": "0.28.2",
- "@esbuild/sunos-x64": "0.28.2",
- "@esbuild/win32-arm64": "0.28.2",
- "@esbuild/win32-ia32": "0.28.2",
- "@esbuild/win32-x64": "0.28.2"
- }
- },
- "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/estree-walker": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
- "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "^1.0.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/eventsource": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-4.1.1.tgz",
- "integrity": "sha512-D6bTRWh6KahHTK/m4WnjPQyEinNPf9eFLEZSEoj7d6fTibspnAVYfzHvirL7u/aoX5d9YYfIkBVAhmigUELk9w==",
- "license": "MIT",
- "dependencies": {
- "eventsource-parser": "^3.0.1"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/eventsource-parser": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz",
- "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==",
- "license": "MIT",
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/expect-type": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
- "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=12.0.0"
- }
- },
- "node_modules/express": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
- "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
- "license": "MIT",
- "dependencies": {
- "accepts": "^2.0.0",
- "body-parser": "^2.2.1",
- "content-disposition": "^1.0.0",
- "content-type": "^1.0.5",
- "cookie": "^0.7.1",
- "cookie-signature": "^1.2.1",
- "debug": "^4.4.0",
- "depd": "^2.0.0",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "etag": "^1.8.1",
- "finalhandler": "^2.1.0",
- "fresh": "^2.0.0",
- "http-errors": "^2.0.0",
- "merge-descriptors": "^2.0.0",
- "mime-types": "^3.0.0",
- "on-finished": "^2.4.1",
- "once": "^1.4.0",
- "parseurl": "^1.3.3",
- "proxy-addr": "^2.0.7",
- "qs": "^6.14.0",
- "range-parser": "^1.2.1",
- "router": "^2.2.0",
- "send": "^1.1.0",
- "serve-static": "^2.2.0",
- "statuses": "^2.0.1",
- "type-is": "^2.0.1",
- "vary": "^1.1.2"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/exsolve": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.3.tgz",
- "integrity": "sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA==",
- "devOptional": true,
- "license": "MIT"
- },
- "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-decode-uri-component": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
- "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==",
- "devOptional": true,
- "license": "MIT"
- },
- "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==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/fast-querystring": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz",
- "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "fast-decode-uri-component": "^1.0.1"
- }
- },
- "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==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fast-uri": {
- "version": "3.1.8",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
- "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
- "devOptional": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/fastify"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/fastify"
- }
- ],
- "license": "BSD-3-Clause"
- },
- "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/feaxios": {
- "version": "0.0.23",
- "resolved": "https://registry.npmjs.org/feaxios/-/feaxios-0.0.23.tgz",
- "integrity": "sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==",
- "license": "MIT",
- "dependencies": {
- "is-retry-allowed": "^3.0.0"
- }
- },
- "node_modules/finalhandler": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
- "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.0",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "on-finished": "^2.4.1",
- "parseurl": "^1.3.3",
- "statuses": "^2.0.1"
- },
- "engines": {
- "node": ">= 18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/find-my-way": {
- "version": "9.7.0",
- "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz",
- "integrity": "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-querystring": "^1.0.0",
- "safe-regex2": "^5.0.0"
- },
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/follow-redirects": {
- "version": "1.16.1",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz",
- "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==",
- "funding": [
- {
- "type": "individual",
- "url": "https://github.com/sponsors/RubenVerborgh"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=4.0"
- },
- "peerDependenciesMeta": {
- "debug": {
- "optional": true
- }
- }
- },
- "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==",
- "devOptional": 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/form-data": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
- "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
- "license": "MIT",
- "dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "es-set-tostringtag": "^2.1.0",
- "hasown": "^2.0.4",
- "mime-types": "^2.1.35"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/form-data/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/form-data/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/formidable": {
- "version": "3.5.4",
- "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
- "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@paralleldrive/cuid2": "^2.2.2",
- "dezalgo": "^1.0.4",
- "once": "^1.4.0"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "url": "https://ko-fi.com/tunnckoCore/commissions"
- }
- },
- "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": "2.0.0",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
- "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "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/generate-function": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
- "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "is-property": "^1.0.2"
- }
- },
- "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-port-please": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz",
- "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==",
- "devOptional": true,
- "license": "MIT"
- },
- "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/giget": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/giget/-/giget-3.3.1.tgz",
- "integrity": "sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==",
- "devOptional": true,
- "license": "MIT",
- "bin": {
- "giget": "dist/cli.mjs"
- }
- },
- "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==",
- "devOptional": true,
- "license": "ISC"
- },
- "node_modules/grammex": {
- "version": "3.1.13",
- "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.13.tgz",
- "integrity": "sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/graphmatch": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz",
- "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==",
- "devOptional": true,
- "license": "MIT"
- },
- "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.4",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
- "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "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/iconv-lite": {
- "version": "0.7.3",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
- "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "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/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/internmap": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
- "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
- "devOptional": true,
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/intl-messageformat": {
- "version": "10.7.18",
- "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz",
- "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@formatjs/ecma402-abstract": "2.3.6",
- "@formatjs/fast-memoize": "2.2.7",
- "@formatjs/icu-messageformat-parser": "2.11.4",
- "tslib": "^2.8.0"
- }
- },
- "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-promise": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
- "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
- "license": "MIT"
- },
- "node_modules/is-property": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
- "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/is-retry-allowed": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-3.0.0.tgz",
- "integrity": "sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "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==",
- "devOptional": true,
- "license": "ISC"
- },
- "node_modules/jiti": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
- "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
- "devOptional": true,
- "license": "MIT",
- "bin": {
- "jiti": "lib/jiti-cli.mjs"
- }
- },
- "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==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/jsonwebtoken": {
- "version": "9.0.3",
- "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
- "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
- "license": "MIT",
- "dependencies": {
- "jws": "^4.0.1",
- "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": "2.0.1",
- "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
- "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
- "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": "4.0.1",
- "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
- "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
- "license": "MIT",
- "dependencies": {
- "jwa": "^2.0.1",
- "safe-buffer": "^5.0.1"
- }
- },
- "node_modules/lodash": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
- "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
- "devOptional": true,
- "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.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.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/long": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
- "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
- "devOptional": true,
- "license": "Apache-2.0"
- },
- "node_modules/lru.min": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.5.tgz",
- "integrity": "sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "bun": ">=1.0.0",
- "deno": ">=1.30.0",
- "node": ">=8.0.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wellwelwel"
- }
- },
- "node_modules/magic-string": {
- "version": "0.30.21",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.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": "1.1.1",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
- "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/merge-descriptors": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
- "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/methods": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
- "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
- "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "mime": "cli.js"
- },
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/mime-db": {
- "version": "1.54.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
- "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
- "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "^1.54.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "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/mysql2": {
- "version": "3.24.4",
- "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.24.4.tgz",
- "integrity": "sha512-A2olluVlj0mvgyIRRISMEzXc51m+21mRtcMVjJyIpt2GG98+XrC9m9HzsqcMsX2LcnfccJvY5NB22g8fENBnOA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "aws-ssl-profiles": "^1.1.2",
- "generate-function": "^2.3.1",
- "iconv-lite": "^0.7.3",
- "long": "^5.3.2",
- "lru.min": "^1.1.4",
- "named-placeholders": "^1.1.6",
- "sql-escaper": "^1.5.1"
- },
- "engines": {
- "node": ">= 8.0"
- },
- "peerDependencies": {
- "@types/node": ">= 8"
- }
- },
- "node_modules/named-placeholders": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
- "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "lru.min": "^1.1.0"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/nanoid": {
- "version": "3.3.19",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
- "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",
- "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/negotiator": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz",
- "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==",
- "license": "MIT",
- "dependencies": {
- "content-type": "^2.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/negotiator/node_modules/content-type": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
- "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "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/obug": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
- "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
- "dev": true,
- "funding": [
- "https://github.com/sponsors/sxzz",
- "https://opencollective.com/debug"
- ],
- "license": "MIT",
- "engines": {
- "node": ">=12.20.0"
- }
- },
- "node_modules/ohash": {
- "version": "2.0.12",
- "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz",
- "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/on-exit-leak-free": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
- "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "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/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/path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-to-regexp": {
- "version": "8.4.2",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
- "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "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/perfect-debounce": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
- "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/pg": {
- "version": "8.23.1",
- "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz",
- "integrity": "sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==",
- "license": "MIT",
- "dependencies": {
- "pg-connection-string": "^2.14.1",
- "pg-pool": "^3.14.0",
- "pg-protocol": "^1.16.1",
- "pg-types": "2.2.0",
- "pgpass": "1.0.5"
- },
- "engines": {
- "node": ">= 16.0.0"
- },
- "optionalDependencies": {
- "pg-cloudflare": "^1.4.1"
- },
- "peerDependencies": {
- "pg-native": ">=3.0.1"
- },
- "peerDependenciesMeta": {
- "pg-native": {
- "optional": true
- }
- }
- },
- "node_modules/pg-cloudflare": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.1.tgz",
- "integrity": "sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/pg-connection-string": {
- "version": "2.14.1",
- "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.1.tgz",
- "integrity": "sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==",
- "license": "MIT"
- },
- "node_modules/pg-int8": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
- "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
- "license": "ISC",
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/pg-pool": {
- "version": "3.14.0",
- "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
- "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
- "license": "MIT",
- "peerDependencies": {
- "pg": ">=8.0"
- }
- },
- "node_modules/pg-protocol": {
- "version": "1.16.1",
- "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.1.tgz",
- "integrity": "sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==",
- "license": "MIT"
- },
- "node_modules/pg-types": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
- "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
- "license": "MIT",
- "dependencies": {
- "pg-int8": "1.0.1",
- "postgres-array": "~2.0.0",
- "postgres-bytea": "~1.0.0",
- "postgres-date": "~1.0.4",
- "postgres-interval": "^1.1.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/pgpass": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
- "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
- "license": "MIT",
- "dependencies": {
- "split2": "^4.1.0"
- }
- },
- "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.7",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
- "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/pino": {
- "version": "9.14.0",
- "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz",
- "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==",
- "license": "MIT",
- "dependencies": {
- "@pinojs/redact": "^0.4.0",
- "atomic-sleep": "^1.0.0",
- "on-exit-leak-free": "^2.1.0",
- "pino-abstract-transport": "^2.0.0",
- "pino-std-serializers": "^7.0.0",
- "process-warning": "^5.0.0",
- "quick-format-unescaped": "^4.0.3",
- "real-require": "^0.2.0",
- "safe-stable-stringify": "^2.3.1",
- "sonic-boom": "^4.0.1",
- "thread-stream": "^3.0.0"
- },
- "bin": {
- "pino": "bin.js"
- }
- },
- "node_modules/pino-abstract-transport": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
- "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
- "license": "MIT",
- "dependencies": {
- "split2": "^4.0.0"
- }
- },
- "node_modules/pino-std-serializers": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
- "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
- "license": "MIT"
- },
- "node_modules/pkg-types": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.3.tgz",
- "integrity": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "confbox": "^0.3.1",
- "exsolve": "^1.1.1",
- "pathe": "^2.0.3"
- }
- },
- "node_modules/pkg-types/node_modules/confbox": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz",
- "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/postcss": {
- "version": "8.5.28",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
- "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
- "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.18",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/postgres": {
- "version": "3.4.7",
- "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz",
- "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==",
- "devOptional": true,
- "license": "Unlicense",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "type": "individual",
- "url": "https://github.com/sponsors/porsager"
- }
- },
- "node_modules/postgres-array": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
- "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/postgres-bytea": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
- "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/postgres-date": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
- "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/postgres-interval": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
- "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
- "license": "MIT",
- "dependencies": {
- "xtend": "^4.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/prisma": {
- "version": "7.10.1",
- "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.10.1.tgz",
- "integrity": "sha512-GoVeErHGGtsSjsflQ1r2BA3RKTXtxNraQJz8OLkLhoLr7OgCscdx4qw4WMM4HnERYc+a1cq2VnMNbep+vOjNcQ==",
- "devOptional": true,
- "hasInstallScript": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@prisma/config": "7.10.1",
- "@prisma/dev": "0.24.17",
- "@prisma/engines": "7.10.1",
- "@prisma/studio-core": "0.33.0",
- "mysql2": "3.24.4",
- "postgres": "3.4.7"
- },
- "bin": {
- "prisma": "build/index.js"
- },
- "engines": {
- "node": "^20.19 || ^22.12 || >=24.0"
- },
- "peerDependencies": {
- "better-sqlite3": ">=9.0.0",
- "typescript": ">=5.4.0"
- },
- "peerDependenciesMeta": {
- "better-sqlite3": {
- "optional": true
- },
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/process-warning": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.1.tgz",
- "integrity": "sha512-oCkNVj4OKnKEcocVBcDfiIugN1/cE6rgfT3nCtzeqZPA8+rIxMaQ0xXzIkGP8Gj2qUaP1UR7uhJ1VD8lUCpzCg==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/fastify"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/fastify"
- }
- ],
- "license": "MIT"
- },
- "node_modules/proper-lockfile": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
- "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "graceful-fs": "^4.2.4",
- "retry": "^0.12.0",
- "signal-exit": "^3.0.2"
- }
- },
- "node_modules/proper-lockfile/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==",
- "devOptional": true,
- "license": "ISC"
- },
- "node_modules/proxy-addr": {
- "version": "2.0.8",
- "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
- "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==",
- "license": "MIT",
- "dependencies": {
- "forwarded": "0.2.0",
- "ipaddr.js": "1.9.1"
- },
- "engines": {
- "node": ">= 0.10"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/proxy-from-env": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
- "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- }
- },
- "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/qs": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
- "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "es-define-property": "^1.0.1",
- "side-channel": "^1.1.1"
- },
- "engines": {
- "node": ">=0.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/quick-format-unescaped": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
- "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
- "license": "MIT"
- },
- "node_modules/range-parser": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
- "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/raw-body": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
- "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
- "license": "MIT",
- "dependencies": {
- "bytes": "~3.1.2",
- "http-errors": "~2.0.1",
- "iconv-lite": "~0.7.0",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/rc9": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.1.0.tgz",
- "integrity": "sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "defu": "^6.1.7",
- "destr": "^2.0.5"
- }
- },
- "node_modules/react": {
- "version": "19.3.1",
- "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz",
- "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-dom": {
- "version": "19.3.1",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz",
- "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==",
- "devOptional": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "scheduler": "^0.28.0"
- },
- "peerDependencies": {
- "react": "^19.3.1"
- }
- },
- "node_modules/readdirp": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz",
- "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">= 20.19.0"
- },
- "funding": {
- "type": "individual",
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/real-require": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
- "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
- "license": "MIT",
- "engines": {
- "node": ">= 12.13.0"
- }
- },
- "node_modules/remeda": {
- "version": "2.33.4",
- "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz",
- "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==",
- "devOptional": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/remeda"
- }
- },
- "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==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/ret": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz",
- "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/retry": {
- "version": "0.12.0",
- "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
- "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">= 4"
- }
- },
- "node_modules/robust-predicates": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
- "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
- "devOptional": true,
- "license": "Unlicense"
- },
- "node_modules/rollup": {
- "version": "4.63.2",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.2.tgz",
- "integrity": "sha512-l5eyksV4tPBj6lJyEa37YzIOCSOV7lkZzEHUdpjWZbtD7wTcFYmEYXSgm5bT4vV+dZLb9rBG1W9GROOG4NS4Ew==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "1.0.9"
- },
- "bin": {
- "rollup": "dist/bin/rollup"
- },
- "engines": {
- "node": ">=18.0.0",
- "npm": ">=8.0.0"
- },
- "optionalDependencies": {
- "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
- "@rollup/rollup-android-arm-eabi": "4.63.2",
- "@rollup/rollup-android-arm64": "4.63.2",
- "@rollup/rollup-darwin-arm64": "4.63.2",
- "@rollup/rollup-darwin-x64": "4.63.2",
- "@rollup/rollup-freebsd-arm64": "4.63.2",
- "@rollup/rollup-freebsd-x64": "4.63.2",
- "@rollup/rollup-linux-arm-gnueabihf": "4.63.2",
- "@rollup/rollup-linux-arm-musleabihf": "4.63.2",
- "@rollup/rollup-linux-arm64-gnu": "4.63.2",
- "@rollup/rollup-linux-arm64-musl": "4.63.2",
- "@rollup/rollup-linux-loong64-gnu": "4.63.2",
- "@rollup/rollup-linux-loong64-musl": "4.63.2",
- "@rollup/rollup-linux-ppc64-gnu": "4.63.2",
- "@rollup/rollup-linux-ppc64-musl": "4.63.2",
- "@rollup/rollup-linux-riscv64-gnu": "4.63.2",
- "@rollup/rollup-linux-riscv64-musl": "4.63.2",
- "@rollup/rollup-linux-s390x-gnu": "4.63.2",
- "@rollup/rollup-linux-x64-gnu": "4.63.2",
- "@rollup/rollup-linux-x64-musl": "4.63.2",
- "@rollup/rollup-openbsd-x64": "4.63.2",
- "@rollup/rollup-openharmony-arm64": "4.63.2",
- "@rollup/rollup-win32-arm64-msvc": "4.63.2",
- "@rollup/rollup-win32-ia32-msvc": "4.63.2",
- "@rollup/rollup-win32-x64-gnu": "4.63.2",
- "@rollup/rollup-win32-x64-msvc": "4.63.2",
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/router": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
- "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.0",
- "depd": "^2.0.0",
- "is-promise": "^4.0.0",
- "parseurl": "^1.3.3",
- "path-to-regexp": "^8.0.0"
- },
- "engines": {
- "node": ">= 18"
- }
- },
- "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/safe-regex2": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.2.tgz",
- "integrity": "sha512-tAtCGFPPP02VroDVzp4daGi6bC7jWG+41D47GKf18FeH/ubvKQHT9VmhAkvtjNOmJwqtApzWeUpEhkyEYULvmw==",
- "devOptional": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/fastify"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/fastify"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "ret": "~0.5.0"
- },
- "bin": {
- "safe-regex2": "bin/safe-regex2.js"
- }
- },
- "node_modules/safe-stable-stringify": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
- "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- }
- },
- "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/scheduler": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
- "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
- "devOptional": true,
- "license": "MIT",
- "peer": true
- },
- "node_modules/semver": {
- "version": "7.8.5",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
- "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/send": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
- "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.3",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "etag": "^1.8.1",
- "fresh": "^2.0.0",
- "http-errors": "^2.0.1",
- "mime-types": "^3.0.2",
- "ms": "^2.1.3",
- "on-finished": "^2.4.1",
- "range-parser": "^1.2.1",
- "statuses": "^2.0.2"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/serve-static": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
- "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
- "license": "MIT",
- "dependencies": {
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "parseurl": "^1.3.3",
- "send": "^1.2.0"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "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==",
- "devOptional": 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==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/side-channel": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
- "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.4",
- "side-channel-list": "^1.0.1",
- "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.1",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
- "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.4"
- },
- "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/siginfo": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
- "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
- "dev": true,
- "license": "ISC"
- },
- "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==",
- "devOptional": true,
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/smol-toml": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz",
- "integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "url": "https://github.com/sponsors/cyyynthia"
- }
- },
- "node_modules/sonic-boom": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
- "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
- "license": "MIT",
- "dependencies": {
- "atomic-sleep": "^1.0.0"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
- "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/split2": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
- "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
- "license": "ISC",
- "engines": {
- "node": ">= 10.x"
- }
- },
- "node_modules/sql-escaper": {
- "version": "1.5.2",
- "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.5.2.tgz",
- "integrity": "sha512-6CKD38c31SENivxOADeMNLdukOnUxUcflKtzVWzace7Riv1v7cAEym5Cx9Q7gYZ3ezIJI7ZpqecQq8cqNeSSFg==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "bun": ">=1.0.0",
- "deno": ">=2.0.0",
- "node": ">=12.0.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
- }
- },
- "node_modules/stackback": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
- "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
- "dev": true,
- "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/std-env": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
- "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/superagent": {
- "version": "10.3.0",
- "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz",
- "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "component-emitter": "^1.3.1",
- "cookiejar": "^2.1.4",
- "debug": "^4.3.7",
- "fast-safe-stringify": "^2.1.1",
- "form-data": "^4.0.5",
- "formidable": "^3.5.4",
- "methods": "^1.1.2",
- "mime": "2.6.0",
- "qs": "^6.14.1"
- },
- "engines": {
- "node": ">=14.18.0"
- }
- },
- "node_modules/supertest": {
- "version": "7.2.2",
- "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz",
- "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cookie-signature": "^1.2.2",
- "methods": "^1.1.2",
- "superagent": "^10.3.0"
- },
- "engines": {
- "node": ">=14.18.0"
- }
- },
- "node_modules/thread-stream": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz",
- "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==",
- "license": "MIT",
- "dependencies": {
- "real-require": "^0.2.0"
- }
- },
- "node_modules/tinybench": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
- "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tinyexec": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
- "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tinyglobby": {
- "version": "0.2.17",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
- "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fdir": "^6.5.0",
- "picomatch": "^4.0.4"
- },
- "engines": {
- "node": ">=12.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/SuperchupuDev"
- }
- },
- "node_modules/tinyrainbow": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
- "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.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/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/tsx": {
- "version": "4.23.13",
- "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz",
- "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "esbuild": "~0.28.0"
- },
- "bin": {
- "tsx": "dist/cli.mjs"
- },
- "engines": {
- "node": ">=18.0.0"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- }
- },
- "node_modules/type-is": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
- "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
- "license": "MIT",
- "dependencies": {
- "content-type": "^2.0.0",
- "media-typer": "^1.1.0",
- "mime-types": "^3.0.0"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/type-is/node_modules/content-type": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
- "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/typescript": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
- "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
- "devOptional": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc"
- },
- "engines": {
- "node": ">=16.20.0"
- },
- "optionalDependencies": {
- "@typescript/typescript-aix-ppc64": "7.0.2",
- "@typescript/typescript-darwin-arm64": "7.0.2",
- "@typescript/typescript-darwin-x64": "7.0.2",
- "@typescript/typescript-freebsd-arm64": "7.0.2",
- "@typescript/typescript-freebsd-x64": "7.0.2",
- "@typescript/typescript-linux-arm": "7.0.2",
- "@typescript/typescript-linux-arm64": "7.0.2",
- "@typescript/typescript-linux-loong64": "7.0.2",
- "@typescript/typescript-linux-mips64el": "7.0.2",
- "@typescript/typescript-linux-ppc64": "7.0.2",
- "@typescript/typescript-linux-riscv64": "7.0.2",
- "@typescript/typescript-linux-s390x": "7.0.2",
- "@typescript/typescript-linux-x64": "7.0.2",
- "@typescript/typescript-netbsd-arm64": "7.0.2",
- "@typescript/typescript-netbsd-x64": "7.0.2",
- "@typescript/typescript-openbsd-arm64": "7.0.2",
- "@typescript/typescript-openbsd-x64": "7.0.2",
- "@typescript/typescript-sunos-x64": "7.0.2",
- "@typescript/typescript-win32-arm64": "7.0.2",
- "@typescript/typescript-win32-x64": "7.0.2"
- }
- },
- "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": "8.9.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
- "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
- "license": "MIT"
- },
- "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/valibot": {
- "version": "1.4.2",
- "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz",
- "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==",
- "devOptional": true,
- "license": "MIT",
- "peerDependencies": {
- "typescript": ">=5"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
- "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/vite": {
- "version": "6.4.3",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
- "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
- "dev": true,
- "license": "MIT",
- "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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vitest": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz",
- "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/expect": "4.1.11",
- "@vitest/mocker": "4.1.11",
- "@vitest/pretty-format": "4.1.11",
- "@vitest/runner": "4.1.11",
- "@vitest/snapshot": "4.1.11",
- "@vitest/spy": "4.1.11",
- "@vitest/utils": "4.1.11",
- "es-module-lexer": "^2.0.0",
- "expect-type": "^1.3.0",
- "magic-string": "^0.30.21",
- "obug": "^2.1.1",
- "pathe": "^2.0.3",
- "picomatch": "^4.0.3",
- "std-env": "^4.0.0-rc.1",
- "tinybench": "^2.9.0",
- "tinyexec": "^1.0.2",
- "tinyglobby": "^0.2.15",
- "tinyrainbow": "^3.1.0",
- "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
- "why-is-node-running": "^2.3.0"
- },
- "bin": {
- "vitest": "vitest.mjs"
- },
- "engines": {
- "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "@edge-runtime/vm": "*",
- "@opentelemetry/api": "^1.9.0",
- "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
- "@vitest/browser-playwright": "4.1.11",
- "@vitest/browser-preview": "4.1.11",
- "@vitest/browser-webdriverio": "4.1.11",
- "@vitest/coverage-istanbul": "4.1.11",
- "@vitest/coverage-v8": "4.1.11",
- "@vitest/ui": "4.1.11",
- "happy-dom": "*",
- "jsdom": "*",
- "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
- },
- "peerDependenciesMeta": {
- "@edge-runtime/vm": {
- "optional": true
- },
- "@opentelemetry/api": {
- "optional": true
- },
- "@types/node": {
- "optional": true
- },
- "@vitest/browser-playwright": {
- "optional": true
- },
- "@vitest/browser-preview": {
- "optional": true
- },
- "@vitest/browser-webdriverio": {
- "optional": true
- },
- "@vitest/coverage-istanbul": {
- "optional": true
- },
- "@vitest/coverage-v8": {
- "optional": true
- },
- "@vitest/ui": {
- "optional": true
- },
- "happy-dom": {
- "optional": true
- },
- "jsdom": {
- "optional": true
- },
- "vite": {
- "optional": false
- }
- }
- },
- "node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/why-is-node-running": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
- "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "siginfo": "^2.0.0",
- "stackback": "0.0.2"
- },
- "bin": {
- "why-is-node-running": "cli.js"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "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/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/zeptomatch": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz",
- "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "grammex": "^3.1.11",
- "graphmatch": "^1.1.0"
- }
- },
- "node_modules/zod": {
- "version": "4.6.5",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz",
- "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/colinhacks"
- }
- }
- }
-}
+{
+ "name": "docforum-core-backend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "docforum-core-backend",
+ "version": "0.0.0",
+ "dependencies": {
+ "@docforum/escrow-sdk": "https://github.com/DocForum/docforum-escrow/releases/download/sdk-v0.2.0/docforum-escrow-sdk-0.2.0.tgz",
+ "@prisma/adapter-pg": "^7.10.1",
+ "@prisma/client": "^7.10.1",
+ "@sendgrid/mail": "^8.1.6",
+ "@stellar/stellar-sdk": "^16.0.1",
+ "bcryptjs": "^3.0.3",
+ "cookie-parser": "^1.4.7",
+ "cors": "^2.8.6",
+ "dotenv": "^17.4.2",
+ "express": "^5.2.1",
+ "intl-messageformat": "^10.7.18",
+ "jsonwebtoken": "^9.0.3",
+ "pg": "^8.23.1",
+ "pino": "^9.13.0",
+ "zod": "^4.6.5"
+ },
+ "devDependencies": {
+ "@types/bcryptjs": "^2.4.6",
+ "@types/cookie-parser": "^1.4.10",
+ "@types/cors": "^2.8.19",
+ "@types/express": "^5.0.6",
+ "@types/jsonwebtoken": "^9.0.10",
+ "@types/node": "^26.5.1",
+ "@types/supertest": "^7.2.1",
+ "embedded-postgres": "^18.4.0-beta.17",
+ "fast-check": "^3.23.2",
+ "prisma": "^7.10.1",
+ "supertest": "^7.2.2",
+ "tsx": "^4.23.13",
+ "typescript": "^7.0.2",
+ "vitest": "^4.1.11"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@docforum/escrow-sdk": {
+ "version": "0.2.0",
+ "resolved": "https://github.com/DocForum/docforum-escrow/releases/download/sdk-v0.2.0/docforum-escrow-sdk-0.2.0.tgz",
+ "integrity": "sha512-M7I6CctDSvimCWT/fa9VIwHGC/qgoO9O9OXQXQqsPXK7JPfN1ndtOFNDnpQlkAYvqnojPSaJ3JpysildOUWUNw==",
+ "dependencies": {
+ "@stellar/stellar-sdk": "^16.0.1"
+ }
+ },
+ "node_modules/@electric-sql/pglite": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.3.tgz",
+ "integrity": "sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@electric-sql/pglite-socket": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.3.tgz",
+ "integrity": "sha512-LAciWM0M1dCL8hlsxu2venbVZcdxema0BtDfpWYVqr+Y468UADw0pFWidhKw1M8sfJ8rdLT71tjMmnirf/IZRQ==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "pglite-server": "dist/scripts/server.js"
+ },
+ "peerDependencies": {
+ "@electric-sql/pglite": "0.4.3"
+ }
+ },
+ "node_modules/@electric-sql/pglite-tools": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.3.tgz",
+ "integrity": "sha512-AlzLJTRJ8+UFgK8CmxIpyIpJ0+YaFw02IiOSdYrqxwPXdSyeIShz8aa9Tq+tYFXdPwcaMp/Fc80mQZ1dkOQ/wg==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@electric-sql/pglite": "0.4.3"
+ }
+ },
+ "node_modules/@embedded-postgres/darwin-arm64": {
+ "version": "18.4.0-beta.17",
+ "resolved": "https://registry.npmjs.org/@embedded-postgres/darwin-arm64/-/darwin-arm64-18.4.0-beta.17.tgz",
+ "integrity": "sha512-Kg1ZMNFzkVIJs3g4V2UYEc5X005km9rGinxFBH8R1sOU2Rblvz4pt2Uf93Pu8Rk273Ue9HIdrbMPpgUqwjUi0Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@embedded-postgres/darwin-x64": {
+ "version": "18.4.0-beta.17",
+ "resolved": "https://registry.npmjs.org/@embedded-postgres/darwin-x64/-/darwin-x64-18.4.0-beta.17.tgz",
+ "integrity": "sha512-4tShSYWMxUQTSWoQAdLnHISvRj6L9gTch9/31ASJPg6wgyN64LDRwMcOINEWybM7N0ropJ3nTYhFT3UX1bKY5Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@embedded-postgres/linux-arm": {
+ "version": "18.4.0-beta.17",
+ "resolved": "https://registry.npmjs.org/@embedded-postgres/linux-arm/-/linux-arm-18.4.0-beta.17.tgz",
+ "integrity": "sha512-VuD1nFasN7yG57zpJcp6MBXM0nXqGfb8GBQV+f1FGJ17+VMNYLIFLhFp99UlY4xnWG74FQC4NPYw8eCwSTJ6qg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@embedded-postgres/linux-arm64": {
+ "version": "18.4.0-beta.17",
+ "resolved": "https://registry.npmjs.org/@embedded-postgres/linux-arm64/-/linux-arm64-18.4.0-beta.17.tgz",
+ "integrity": "sha512-TIzjtGnDGSD/LbdW0OWCEcbI+YVT0WKSfSr20TCkkP4UR8zeKe+QCZbO0/CM4hS9EWyZ4cDebjRvpRc3iMi6wg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@embedded-postgres/linux-ia32": {
+ "version": "18.4.0-beta.17",
+ "resolved": "https://registry.npmjs.org/@embedded-postgres/linux-ia32/-/linux-ia32-18.4.0-beta.17.tgz",
+ "integrity": "sha512-aSRe4kknqRHuedtpo/rDIaqa8r9VrKIgW9p5FkJyIFqUUkIkxRqND1dg8xrDRREcSUXRJKS0x+j/AuxflgYIfg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@embedded-postgres/linux-ppc64": {
+ "version": "18.4.0-beta.17",
+ "resolved": "https://registry.npmjs.org/@embedded-postgres/linux-ppc64/-/linux-ppc64-18.4.0-beta.17.tgz",
+ "integrity": "sha512-zO2RRUFdKRPplrdhmglG39VXJZzDXu3P3ONAG1sFPhGh89vbk24Btd5P7uOmQLIpWehzL0s+UqUWoeE/ZUKvgw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@embedded-postgres/linux-x64": {
+ "version": "18.4.0-beta.17",
+ "resolved": "https://registry.npmjs.org/@embedded-postgres/linux-x64/-/linux-x64-18.4.0-beta.17.tgz",
+ "integrity": "sha512-jVw/MdDtIX/vICH/DKIe6/mHpiCggdx6QVyza4vt/NbcZFsL0KhwglF6F1Koqx3gRBZ9XtN+vi63EsqSyqOSxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@embedded-postgres/windows-x64": {
+ "version": "18.4.0-beta.17",
+ "resolved": "https://registry.npmjs.org/@embedded-postgres/windows-x64/-/windows-x64-18.4.0-beta.17.tgz",
+ "integrity": "sha512-AwRerliA4IGWyW5jWBvHt5vidVUSB0QQW9Tt2y7ScnmifnCb/awfxZr4BkBSTv+gNt8Djcddqs+xSF5Z6/CkTg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
+ "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
+ "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
+ "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
+ "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
+ "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
+ "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
+ "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
+ "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
+ "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
+ "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
+ "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
+ "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
+ "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
+ "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
+ "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
+ "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
+ "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
+ "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
+ "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
+ "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
+ "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@formatjs/ecma402-abstract": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz",
+ "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@formatjs/fast-memoize": "2.2.7",
+ "@formatjs/intl-localematcher": "0.6.2",
+ "decimal.js": "^10.4.3",
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@formatjs/fast-memoize": {
+ "version": "2.2.7",
+ "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
+ "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@formatjs/icu-messageformat-parser": {
+ "version": "2.11.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz",
+ "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "2.3.6",
+ "@formatjs/icu-skeleton-parser": "1.8.16",
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@formatjs/icu-skeleton-parser": {
+ "version": "1.8.16",
+ "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz",
+ "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "2.3.6",
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@formatjs/intl-localematcher": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz",
+ "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@napi-rs/lzma-linux-x64-gnu": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
+ "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^22.20 || ^24.12 || >=25"
+ }
+ },
+ "node_modules/@noble/ed25519": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-3.2.0.tgz",
+ "integrity": "sha512-criDgRlnUA09hchYrTy/JUWPIEap5rZxQe6wDWzRx51oWWpDRcUpuNzlgPxDJaOK6AsW9c0wKcj3rKRv6t+bPQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/hashes": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@paralleldrive/cuid2": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
+ "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "^1.1.5"
+ }
+ },
+ "node_modules/@pinojs/redact": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
+ "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
+ "license": "MIT"
+ },
+ "node_modules/@prisma/adapter-pg": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.10.1.tgz",
+ "integrity": "sha512-EP9ruPYHjKP80WZlb5qVaEXKgZjJieBFjAxcBtQnDAHD6+16UN4keVmBh6L/RUpuDQT2FQRbvKNgq5d4Ix+IkQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/driver-adapter-utils": "7.10.1",
+ "@types/pg": "^8.16.0",
+ "pg": "^8.16.3",
+ "postgres-array": "3.0.4"
+ }
+ },
+ "node_modules/@prisma/adapter-pg/node_modules/postgres-array": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
+ "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@prisma/client": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.10.1.tgz",
+ "integrity": "sha512-5tgAbFGh//zVOC6WPNVgziOErJHYkUE+wmqCwbIyq6IrTXUYtv9y2iBFGh/Owo99Mx0+cr0j1FOrxkYBMuyhaQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/client-runtime-utils": "7.10.1"
+ },
+ "engines": {
+ "node": "^20.19 || ^22.12 || >=24.0"
+ },
+ "peerDependencies": {
+ "prisma": "*",
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "prisma": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@prisma/client-runtime-utils": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.10.1.tgz",
+ "integrity": "sha512-AzoATVIgmO3lcZfM443+9SI7+cyLMW2+Psw4op8WQHpHvHCX//CKxv8fwhiL0yGWF8DjdQrFewxrZo5McBdB8g==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/config": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.10.1.tgz",
+ "integrity": "sha512-+0swF5ELUa3P/9cIR26+gpYjjCbGE0qvEBZhV4zMDkYsrtHjZ884MQbOnWFWXj600N0sSJNnHbtBqe12yDngWg==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "c12": "3.3.4",
+ "deepmerge-ts": "8.0.2",
+ "effect": "3.20.0",
+ "empathic": "2.0.0"
+ }
+ },
+ "node_modules/@prisma/debug": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.10.1.tgz",
+ "integrity": "sha512-eFCAkGZ13D/qPg48JBlG5IWi9cSBBOC7oG3abDPcAkYioDWlIjh0YJJLK2J7qo5S+xWWD8I6egAN/8KbCaiMCA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/dev": {
+ "version": "0.24.17",
+ "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.17.tgz",
+ "integrity": "sha512-UvdZzmpFwknnfreh6Jije84ekkYGPYEJhXG1tFzCsCfQyzJifrOo38eZc0qajzvaC6OLUOrN9ML5XfCnEZL9DA==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "@electric-sql/pglite": "0.4.3",
+ "@electric-sql/pglite-socket": "0.1.3",
+ "@electric-sql/pglite-tools": "0.3.3",
+ "@prisma/get-platform": "7.2.0",
+ "@prisma/query-plan-executor": "7.2.0",
+ "@prisma/streams-local": "0.1.11",
+ "find-my-way": "9.7.0",
+ "foreground-child": "3.3.1",
+ "get-port-please": "3.2.0",
+ "pathe": "2.0.3",
+ "proper-lockfile": "4.1.2",
+ "remeda": "2.33.4",
+ "std-env": "3.10.0",
+ "valibot": "1.4.2",
+ "zeptomatch": "2.1.0"
+ }
+ },
+ "node_modules/@prisma/dev/node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@prisma/driver-adapter-utils": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.10.1.tgz",
+ "integrity": "sha512-DJ4c6xvVnm3s9ZUyql9nCUicADSqyvH+CjXRCM9L6m5zJxCNmebCElG6X4irICq3rosBwlX+S19Dxq/4IrZk3A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.10.1"
+ }
+ },
+ "node_modules/@prisma/engines": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.10.1.tgz",
+ "integrity": "sha512-R6/ghv1Bu36FhCdQLFof1pNtyoWoeDzJLYeTKjgqb4eGtOAJx7ZqJtcm4pzkahLfZ9rYz8AkmsMDwkyiYbXPCQ==",
+ "devOptional": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.10.1",
+ "@prisma/engines-version": "8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8",
+ "@prisma/fetch-engine": "7.10.1",
+ "@prisma/get-platform": "7.10.1"
+ }
+ },
+ "node_modules/@prisma/engines-version": {
+ "version": "8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8.tgz",
+ "integrity": "sha512-XjXmwjN2NE340RpXoXv8ZF8aNAvr0FE+7gNfkKeW6vyjXs7F8uxvEbWQoVrhioXDQoIsnNmv7r+wxvzfdEZaWA==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/engines/node_modules/@prisma/get-platform": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.1.tgz",
+ "integrity": "sha512-iVnghHne5vcaobBjPVCMEjh5r/1UCj394EIGTUclasFRatpR/MQwcM2veLv5kgoFSyYP54nch/lNFCQRYcDxvg==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.10.1"
+ }
+ },
+ "node_modules/@prisma/fetch-engine": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.10.1.tgz",
+ "integrity": "sha512-qZezeaExLWG2ptbHOrMxdiWluKHgqEclTdJbvY6pk6+Wg7EiVkeq5U3cXz9ti8nd0bngNvsDgledLHVRg5LF1w==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.10.1",
+ "@prisma/engines-version": "8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8",
+ "@prisma/get-platform": "7.10.1"
+ }
+ },
+ "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.1.tgz",
+ "integrity": "sha512-iVnghHne5vcaobBjPVCMEjh5r/1UCj394EIGTUclasFRatpR/MQwcM2veLv5kgoFSyYP54nch/lNFCQRYcDxvg==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.10.1"
+ }
+ },
+ "node_modules/@prisma/get-platform": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz",
+ "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.2.0"
+ }
+ },
+ "node_modules/@prisma/get-platform/node_modules/@prisma/debug": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz",
+ "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/query-plan-executor": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz",
+ "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/streams-local": {
+ "version": "0.1.11",
+ "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.11.tgz",
+ "integrity": "sha512-0TcebL559MByKqTJ+SsrFIEg228iw8UCVRFckzgfRSiJqczhs+MuAgWOF9lnOIV/IVqvu+KMnFTH0eDeTQMpUg==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "ajv": "^8.12.0",
+ "better-result": "^2.7.0",
+ "env-paths": "^3.0.0",
+ "proper-lockfile": "^4.1.2"
+ },
+ "engines": {
+ "bun": ">=1.2.0",
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@prisma/studio-core": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.33.0.tgz",
+ "integrity": "sha512-V2fX/nKEymNTrHXwfP26PGjoLStO35Ogu+ex7CFJbLrMYEcZxxZpiSNOs7px23Hk5mzLWvM5RsqG6Ka+rha+wg==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@radix-ui/react-toggle": "1.1.10",
+ "@visx/curve": "4.0.1-alpha.0",
+ "@visx/event": "4.0.1-alpha.0",
+ "@visx/grid": "4.0.1-alpha.0",
+ "@visx/group": "4.0.1-alpha.0",
+ "@visx/responsive": "4.0.1-alpha.0",
+ "@visx/scale": "4.0.1-alpha.0",
+ "@visx/shape": "4.0.1-alpha.0",
+ "d3-array": "3.2.4",
+ "d3-shape": "3.2.0",
+ "elkjs": "0.11.1"
+ },
+ "engines": {
+ "node": "^20.19 || ^22.12 || >=24.0",
+ "pnpm": "8"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
+ "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.2.tgz",
+ "integrity": "sha512-Xa6RDoWa+hNiX6PgsljlH6W75RaONx3y6PVlbLhkEWW+GaPQ3dP5gwbL/erAzQHWwkvW5UxdD5l87Qx2FAQ/4A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.2.tgz",
+ "integrity": "sha512-vNASxsghMfQ5s+v3PrpnJd+ryL/26lxCCaGI+sDJ7VzmHiYXIrrVltsDhaawxLM1WcoMU2oYlbPHLaYQtBzhcg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.2.tgz",
+ "integrity": "sha512-0dWDjmlrpZAgjPD/aPzUDhBW8APLRjAni5bOrM76wiiZm+E+KTMVKNhAzaTBohz8UyO2fKNAl0+fygbe2HZXOA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.2.tgz",
+ "integrity": "sha512-N58uktcwzk3+qT4KHEuNdIxX1N01RWrkfVoml69EAbSaNDL+sbNVLx2RMl4Qd23lpA0fgPvyh5hHb4weD5WKmg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.2.tgz",
+ "integrity": "sha512-HWF2zH8EAp2scWRpt2PGe6iUGz7zi04waXsdRr3zb4DWCk2ImIo5FZu0jjmD53nP/DGSvnW0e7/1ToCNZs2lZw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.2.tgz",
+ "integrity": "sha512-MkvcwHMnzPSMOQEwB6wHnLzmc+hT8BGc5bW/Mhmjjgx3wbj6VBnlc47XsK74kD0K9MikFfXpQqyz4NUXaUW62A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.2.tgz",
+ "integrity": "sha512-xe1bCKPJaKsD0tfd7Rb6bGfUogJTpKbTEEthsfdb7hTfTRNJVQTdirabQx0o6ERVba/smkM720soMY+0QnrlSQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.2.tgz",
+ "integrity": "sha512-yOM7LdK0p6gk6+Q773OEwtlsikT1TL3yMmYsTtRlDRPha5vV2DC5x7LqRWDr6f3cSYNMKVqxzffXv8ivxNBIFQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.2.tgz",
+ "integrity": "sha512-qiWuJJV3DybA2IfzvRimeKXGrGuVPv1zobSY/26KnP3HbV0VcNb3ECzgvtbvF3xjSMkcooou6HASXZuLdjnhpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.2.tgz",
+ "integrity": "sha512-akcZquRzCY/KpUoZAMBhGf7oi4LmXq1BzRA5CPAC3rkUf28Y/sAYV3jSL+JKd7cwEyFvR5G0XVZ0gaMedP+60A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.2.tgz",
+ "integrity": "sha512-fNwYHrPyYyxauPzX/cpYw8Z7LQpp+DGA0KCoswA0aVFBpmdMil9XgjB8V3Ny64Ihu797+GKcuJqnsOKEmor7fA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.2.tgz",
+ "integrity": "sha512-XfvsgzR7DZqREdst7K1Mj3ilSUM5xLAHJcIMDFPKdxTs9q5VHOT8aMA+a683fqBu7DQl8+Sd9HCsQYL8EMY9qA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.2.tgz",
+ "integrity": "sha512-Pp7gVZggEFlbcuztay+/U0gVG9S1XAh8i7I1Re/htbAzo43P5wHZHw6pTyzotISqlKohoh9RpIfnOz3RbemK1w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.2.tgz",
+ "integrity": "sha512-zkgL2xff6i7u5hau/m6FGeS8gRkLEdgLw522WGmdWWlLd9btmNl3S80mcEjtGq+kvgUekQ3+BOYLLLcPlS2LIA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.2.tgz",
+ "integrity": "sha512-qOheJomrkVCbbHFJ7L3J97cnhfogKqguAQphv26+3ZsAQIF1L19b+dArl//s8rjJHJLz9byykyM8NBP4nmSa1g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.2.tgz",
+ "integrity": "sha512-XlxLD54wQhH3FciCgMofxBw27NzUe818gJH410qWvc41UT0ZFcgxVjyX5/EK8MPTupjeVWqN5oy+9pCA9mqfCA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.2.tgz",
+ "integrity": "sha512-vdryWeRb2bLJZf0Fv/W8se6nvsHe2PkTCxV0meheK3nQE+G90VCJcke51Miy1yQRsfm2uqIyjXOu4wmUzbTtkQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.2.tgz",
+ "integrity": "sha512-bcq2h2pkKmH2po4cZV8VWzO4lL40STyu/nLoFpYMQp9C2tCVNTdcVv86MwSsn3D5s1FBe2Ty1atqvVAUTMimNg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.2.tgz",
+ "integrity": "sha512-EGoo5DMVMRkTId8fuTDaoxVlR5ZTsKULUezRjd9gCw5eeY+DjCvDpZAOlNUvKPGX+7rS1RWx6j+yOpNPx0cUgQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.2.tgz",
+ "integrity": "sha512-MErl12k7BFHZG1TI9QF/3lSSZARzq9KgNy/FjnqFMCkv+N4RSSzoUCA5h2mqHX4Mox3WaTVKblyzhQ1zRb2ZuQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.2.tgz",
+ "integrity": "sha512-ILs8k07Wh4p0PsNY4wYLEaXZKMOpVhrG5QDB0yHhGhuzOfDlnyHN6sflL4El/MpUP1y8uY2lUZrv4oBS6pTT3g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.2.tgz",
+ "integrity": "sha512-hKgB3nz/TKD3Wv78XEsyXzQsNjvhOHmwKQTvXADGOyU/cIClZDO7DsoggbdmJDPGp5V80tA3Vfv61PaKTLH3LA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.2.tgz",
+ "integrity": "sha512-T4wf1mudIDxN8Q/CWIBJC1u5gQUc+r5mPvlwoSbIvNkyVTP2TAFeobEmst5AQ4gMyAz4sSByVdoTDfvTmGK/8g==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.2.tgz",
+ "integrity": "sha512-tC3IY7qoaD9Ll3/8WJQn49j5V2f/NuI9S41NOE2iM5MPs3sPIvOkVToLcz/7Bz4pyF7PSvrtwu8I/pUrGOSecQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.2.tgz",
+ "integrity": "sha512-6NHnk/K3eq2ZFYcU1X8g67s9qIJRCOTT92gwLMVBp08dB2uuuwI1/Q/empzL2Bfr2f2WRLJVwpp90RmacQyFkw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@sendgrid/client": {
+ "version": "8.1.6",
+ "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.6.tgz",
+ "integrity": "sha512-/BHu0hqwXNHr2aLhcXU7RmmlVqrdfrbY9KpaNj00KZHlVOVoRxRVrpOCabIB+91ISXJ6+mLM9vpaVUhK6TwBWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@sendgrid/helpers": "^8.0.0",
+ "axios": "^1.12.0"
+ },
+ "engines": {
+ "node": ">=12.*"
+ }
+ },
+ "node_modules/@sendgrid/helpers": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz",
+ "integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==",
+ "license": "MIT",
+ "dependencies": {
+ "deepmerge": "^4.2.2"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ }
+ },
+ "node_modules/@sendgrid/mail": {
+ "version": "8.1.6",
+ "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.6.tgz",
+ "integrity": "sha512-/ZqxUvKeEztU9drOoPC/8opEPOk+jLlB2q4+xpx6HVLq6aFu3pMpalkTpAQz8XfRfpLp8O25bh6pGPcHDCYpqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@sendgrid/client": "^8.1.5",
+ "@sendgrid/helpers": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=12.*"
+ }
+ },
+ "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/@stellar/js-xdr": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-4.0.0.tgz",
+ "integrity": "sha512-+NmNa7Tk5BI5XFdy/6xGTqAN4J9a9KgCrCGhj2uEUTCBhLkch0M+QbKzNH8zEnejWe0p8w+0q5hUVX6L3OzoVA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20.0.0",
+ "pnpm": ">=9.0.0"
+ }
+ },
+ "node_modules/@stellar/stellar-sdk": {
+ "version": "16.3.1",
+ "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-16.3.1.tgz",
+ "integrity": "sha512-PPM1JA51NChSPyMkh+T9sHxJtP/v9e9Gcyc2BMNvnGvbZwqoKJclRSdPgmalLDAmTnTewGs7VMM9nRxDeCzzgQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@noble/ed25519": "^3.1.0",
+ "@noble/hashes": "^2.2.0",
+ "@stellar/js-xdr": "4.0.0",
+ "axios": "1.18.0",
+ "base32.js": "^0.1.0",
+ "bignumber.js": "^11.1.4",
+ "buffer": "^6.0.3",
+ "commander": "^14.0.3",
+ "eventsource": "^4.1.0",
+ "feaxios": "^0.0.23",
+ "smol-toml": "^1.6.1",
+ "uint8array-extras": "^1.5.0"
+ },
+ "bin": {
+ "stellar-js": "bin/stellar-js"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@stellar/stellar-sdk/node_modules/@noble/hashes": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
+ "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@types/bcryptjs": {
+ "version": "2.4.6",
+ "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
+ "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "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/cookiejar": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
+ "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.19",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz",
+ "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz",
+ "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz",
+ "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+ "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz",
+ "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-time-format": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz",
+ "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "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",
+ "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.3",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz",
+ "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "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/jsonwebtoken": {
+ "version": "9.0.10",
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
+ "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/lodash": {
+ "version": "4.17.26",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.26.tgz",
+ "integrity": "sha512-48NWfiY6oNcKOh47Z4h+sIvwcQR91h6KPhaYKgeMIrkT2jV82Gy90SErj60vW8jQbeMSOSbhIv3bZ7JoqPPVhA==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/methods": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
+ "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "26.5.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.1.tgz",
+ "integrity": "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~8.9.0"
+ }
+ },
+ "node_modules/@types/pg": {
+ "version": "8.23.1",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz",
+ "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^2.2.0"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
+ "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/react": {
+ "version": "19.3.0",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
+ "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "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/superagent": {
+ "version": "8.1.11",
+ "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.11.tgz",
+ "integrity": "sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/cookiejar": "^2.1.5",
+ "@types/methods": "^1.1.4",
+ "@types/node": "*",
+ "form-data": "^4.0.0"
+ }
+ },
+ "node_modules/@types/supertest": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.1.tgz",
+ "integrity": "sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/methods": "^1.1.4",
+ "@types/superagent": "^8.1.0"
+ }
+ },
+ "node_modules/@typescript/typescript-aix-ppc64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
+ "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-darwin-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
+ "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-darwin-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
+ "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-freebsd-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
+ "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-freebsd-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
+ "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-arm": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
+ "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
+ "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-loong64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
+ "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-mips64el": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
+ "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-ppc64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
+ "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-riscv64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
+ "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-s390x": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
+ "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
+ "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-netbsd-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
+ "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-netbsd-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
+ "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-openbsd-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
+ "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-openbsd-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
+ "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-sunos-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
+ "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-win32-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
+ "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-win32-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
+ "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@visx/curve": {
+ "version": "4.0.1-alpha.0",
+ "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-4.0.1-alpha.0.tgz",
+ "integrity": "sha512-jRu61Uz274pV1zyioXmboyrLutYbnKsgjj4njSGCnhdXj5GkZvZbg+ThDb6oOzoAnJOBRLz4rzPlWvNJOzuVMg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@visx/vendor": "4.0.0-alpha.0"
+ }
+ },
+ "node_modules/@visx/event": {
+ "version": "4.0.1-alpha.0",
+ "resolved": "https://registry.npmjs.org/@visx/event/-/event-4.0.1-alpha.0.tgz",
+ "integrity": "sha512-EQqCMSv/s8NbFjo+hz3FKsvvYfP+2QslsFJ/24/O5l/W+7UC6J6aAvO0ujVwrTwdYbuQ+vhxKi1xdPdKR/qj1g==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*",
+ "@visx/point": "4.0.1-alpha.0"
+ }
+ },
+ "node_modules/@visx/grid": {
+ "version": "4.0.1-alpha.0",
+ "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-4.0.1-alpha.0.tgz",
+ "integrity": "sha512-rycutGmTHO+znNdPumheWMglm7YfpffvRwUkVy5zy4WoORIuKTMkDxwnOzHG2xMxU3EE/YCd37xFV5AxA30yeg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*",
+ "@visx/curve": "4.0.1-alpha.0",
+ "@visx/group": "4.0.1-alpha.0",
+ "@visx/point": "4.0.1-alpha.0",
+ "@visx/scale": "4.0.1-alpha.0",
+ "@visx/shape": "4.0.1-alpha.0",
+ "classnames": "^2.3.1"
+ },
+ "peerDependencies": {
+ "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0"
+ }
+ },
+ "node_modules/@visx/group": {
+ "version": "4.0.1-alpha.0",
+ "resolved": "https://registry.npmjs.org/@visx/group/-/group-4.0.1-alpha.0.tgz",
+ "integrity": "sha512-V19l7iQ7jccBv8kao/EByuI6o4xtxzzLV9nqVI1hRvmdzTVsuLpqlwzYCZUXJaTVvUWf8s4D2SQFjGkj/Nw+0w==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*",
+ "classnames": "^2.3.1"
+ },
+ "peerDependencies": {
+ "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0"
+ }
+ },
+ "node_modules/@visx/point": {
+ "version": "4.0.1-alpha.0",
+ "resolved": "https://registry.npmjs.org/@visx/point/-/point-4.0.1-alpha.0.tgz",
+ "integrity": "sha512-ijTfr/Nx09f03vIj9nyTr3z4Xth4Y75427UaogJh6dnIRLMEFHQOwNu791sbfiNj0a+ZXuaE32h0vKrFe4/8Qg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@visx/responsive": {
+ "version": "4.0.1-alpha.0",
+ "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-4.0.1-alpha.0.tgz",
+ "integrity": "sha512-o+1zGywQZY0+yOx3Iw87wc4bbPJRr/HnIukTwfOz4UVyj9pB1OQNVHB7OORO1+LBHJceWpB31co/ZV9KHncKrA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/lodash": "^4.17.13",
+ "@types/react": "*",
+ "lodash": "^4.17.21"
+ },
+ "peerDependencies": {
+ "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0"
+ }
+ },
+ "node_modules/@visx/scale": {
+ "version": "4.0.1-alpha.0",
+ "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-4.0.1-alpha.0.tgz",
+ "integrity": "sha512-nzjeE87vFSAXGWFiiNfBpNLAf0Q8Qmf6syvKLjqNi4kGZkdhbUll3E/59YsgWXmjM8+llPLWzGsP+JPvo5eq1A==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@visx/vendor": "4.0.0-alpha.0"
+ }
+ },
+ "node_modules/@visx/shape": {
+ "version": "4.0.1-alpha.0",
+ "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-4.0.1-alpha.0.tgz",
+ "integrity": "sha512-62QeiVNmPlterQGwhkEDcbq7M0MqY0lBsK5QKXtM9ZoPZWkuGV3aykA3+Xu20B2FAvyJq4LqJzBc7Sxr+EAdbA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/lodash": "^4.17.13",
+ "@types/react": "*",
+ "@visx/curve": "4.0.1-alpha.0",
+ "@visx/group": "4.0.1-alpha.0",
+ "@visx/scale": "4.0.1-alpha.0",
+ "@visx/vendor": "4.0.0-alpha.0",
+ "classnames": "^2.3.1",
+ "lodash": "^4.17.21"
+ },
+ "peerDependencies": {
+ "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0"
+ }
+ },
+ "node_modules/@visx/vendor": {
+ "version": "4.0.0-alpha.0",
+ "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-4.0.0-alpha.0.tgz",
+ "integrity": "sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==",
+ "devOptional": true,
+ "license": "MIT and ISC",
+ "dependencies": {
+ "@types/d3-array": "3.0.3",
+ "@types/d3-color": "3.1.0",
+ "@types/d3-delaunay": "6.0.1",
+ "@types/d3-format": "3.0.1",
+ "@types/d3-geo": "3.1.0",
+ "@types/d3-interpolate": "3.0.1",
+ "@types/d3-path": "3.1.1",
+ "@types/d3-scale": "4.0.2",
+ "@types/d3-shape": "3.1.7",
+ "@types/d3-time": "3.0.0",
+ "@types/d3-time-format": "2.1.0",
+ "d3-array": "3.2.1",
+ "d3-color": "3.1.0",
+ "d3-delaunay": "6.0.2",
+ "d3-format": "3.1.0",
+ "d3-geo": "3.1.0",
+ "d3-interpolate": "3.0.1",
+ "d3-path": "3.1.0",
+ "d3-scale": "4.0.2",
+ "d3-shape": "3.2.0",
+ "d3-time": "3.1.0",
+ "d3-time-format": "4.1.0",
+ "internmap": "2.0.3"
+ }
+ },
+ "node_modules/@visx/vendor/node_modules/d3-array": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz",
+ "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz",
+ "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.11",
+ "@vitest/utils": "4.1.11",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz",
+ "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.11",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz",
+ "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz",
+ "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.11",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz",
+ "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.11",
+ "@vitest/utils": "4.1.11",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz",
+ "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz",
+ "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.11",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "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.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/async-exit-hook": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz",
+ "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.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/atomic-sleep": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
+ "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/aws-ssl-profiles": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
+ "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/axios": {
+ "version": "1.18.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz",
+ "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.16.0",
+ "form-data": "^4.0.5",
+ "https-proxy-agent": "^5.0.1",
+ "proxy-from-env": "^2.1.0"
+ }
+ },
+ "node_modules/base32.js": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz",
+ "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "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==",
+ "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/bcryptjs": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
+ "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
+ "license": "BSD-3-Clause",
+ "bin": {
+ "bcrypt": "bin/bcrypt"
+ }
+ },
+ "node_modules/better-result": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.10.0.tgz",
+ "integrity": "sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/bignumber.js": {
+ "version": "11.1.5",
+ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.5.tgz",
+ "integrity": "sha512-6WmzCNtUnfKpbozq+hOgWaZMMzORmYBwF1xZScyoIX3QRYWeKTtxxwDOW5tIz7C9BdjkIYHGTcelCLkXg0mndw==",
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
+ "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^2.0.0",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.1",
+ "iconv-lite": "^0.7.2",
+ "on-finished": "^2.4.1",
+ "qs": "^6.15.2",
+ "raw-body": "^3.0.2",
+ "type-is": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/body-parser/node_modules/content-type": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "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.2.1"
+ }
+ },
+ "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/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.3.4",
+ "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz",
+ "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^5.0.0",
+ "confbox": "^0.2.4",
+ "defu": "^6.1.6",
+ "dotenv": "^17.3.1",
+ "exsolve": "^1.0.8",
+ "giget": "^3.2.0",
+ "jiti": "^2.6.1",
+ "ohash": "^2.0.11",
+ "pathe": "^2.0.3",
+ "perfect-debounce": "^2.1.0",
+ "pkg-types": "^2.3.0",
+ "rc9": "^3.0.1"
+ },
+ "peerDependencies": {
+ "magicast": "*"
+ },
+ "peerDependenciesMeta": {
+ "magicast": {
+ "optional": true
+ }
+ }
+ },
+ "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/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+ "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/classnames": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+ "devOptional": 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/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/component-emitter": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
+ "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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/content-disposition": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "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-parser/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/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cookiejar": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
+ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "devOptional": 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==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "devOptional": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz",
+ "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "devOptional": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "devOptional": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "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/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "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==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/deepmerge-ts": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-8.0.2.tgz",
+ "integrity": "sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "ko-fi",
+ "url": "https://ko-fi.com/rebeccastevens"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/deepmerge-ts"
+ }
+ ],
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=16.9.0"
+ }
+ },
+ "node_modules/defu": {
+ "version": "6.1.7",
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
+ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/delaunator": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
+ "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
+ "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/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/dezalgo": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
+ "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "asap": "^2.0.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.4.2",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
+ "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "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/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.20.0",
+ "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz",
+ "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "fast-check": "^3.23.1"
+ }
+ },
+ "node_modules/elkjs": {
+ "version": "0.11.1",
+ "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.11.1.tgz",
+ "integrity": "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==",
+ "devOptional": true,
+ "license": "EPL-2.0"
+ },
+ "node_modules/embedded-postgres": {
+ "version": "18.4.0-beta.17",
+ "resolved": "https://registry.npmjs.org/embedded-postgres/-/embedded-postgres-18.4.0-beta.17.tgz",
+ "integrity": "sha512-ORT/A1YiO6ecpRHpyIIBgyCR/8ASqqYZuFw11aX9PkqTX3FUM5+9sNOXY1mGIixxsm2TWSIA5WghJEk7A5ZxVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async-exit-hook": "^2.0.1",
+ "pg": "^8.7.3"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "optionalDependencies": {
+ "@embedded-postgres/darwin-arm64": "^18.4.0-beta.17",
+ "@embedded-postgres/darwin-x64": "^18.4.0-beta.17",
+ "@embedded-postgres/linux-arm": "^18.4.0-beta.17",
+ "@embedded-postgres/linux-arm64": "^18.4.0-beta.17",
+ "@embedded-postgres/linux-ia32": "^18.4.0-beta.17",
+ "@embedded-postgres/linux-ppc64": "^18.4.0-beta.17",
+ "@embedded-postgres/linux-x64": "^18.4.0-beta.17",
+ "@embedded-postgres/windows-x64": "^18.4.0-beta.17"
+ }
+ },
+ "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/env-paths": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
+ "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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.3.2",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
+ "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "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.28.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
+ "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.2",
+ "@esbuild/android-arm": "0.28.2",
+ "@esbuild/android-arm64": "0.28.2",
+ "@esbuild/android-x64": "0.28.2",
+ "@esbuild/darwin-arm64": "0.28.2",
+ "@esbuild/darwin-x64": "0.28.2",
+ "@esbuild/freebsd-arm64": "0.28.2",
+ "@esbuild/freebsd-x64": "0.28.2",
+ "@esbuild/linux-arm": "0.28.2",
+ "@esbuild/linux-arm64": "0.28.2",
+ "@esbuild/linux-ia32": "0.28.2",
+ "@esbuild/linux-loong64": "0.28.2",
+ "@esbuild/linux-mips64el": "0.28.2",
+ "@esbuild/linux-ppc64": "0.28.2",
+ "@esbuild/linux-riscv64": "0.28.2",
+ "@esbuild/linux-s390x": "0.28.2",
+ "@esbuild/linux-x64": "0.28.2",
+ "@esbuild/netbsd-arm64": "0.28.2",
+ "@esbuild/netbsd-x64": "0.28.2",
+ "@esbuild/openbsd-arm64": "0.28.2",
+ "@esbuild/openbsd-x64": "0.28.2",
+ "@esbuild/openharmony-arm64": "0.28.2",
+ "@esbuild/sunos-x64": "0.28.2",
+ "@esbuild/win32-arm64": "0.28.2",
+ "@esbuild/win32-ia32": "0.28.2",
+ "@esbuild/win32-x64": "0.28.2"
+ }
+ },
+ "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/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.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/eventsource": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-4.1.1.tgz",
+ "integrity": "sha512-D6bTRWh6KahHTK/m4WnjPQyEinNPf9eFLEZSEoj7d6fTibspnAVYfzHvirL7u/aoX5d9YYfIkBVAhmigUELk9w==",
+ "license": "MIT",
+ "dependencies": {
+ "eventsource-parser": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/eventsource-parser": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz",
+ "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/exsolve": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.3.tgz",
+ "integrity": "sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "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-decode-uri-component": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
+ "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "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==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-querystring": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz",
+ "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-decode-uri-component": "^1.0.1"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
+ "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "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/feaxios": {
+ "version": "0.0.23",
+ "resolved": "https://registry.npmjs.org/feaxios/-/feaxios-0.0.23.tgz",
+ "integrity": "sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==",
+ "license": "MIT",
+ "dependencies": {
+ "is-retry-allowed": "^3.0.0"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/find-my-way": {
+ "version": "9.7.0",
+ "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz",
+ "integrity": "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-querystring": "^1.0.0",
+ "safe-regex2": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz",
+ "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "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==",
+ "devOptional": 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/form-data": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.4",
+ "mime-types": "^2.1.35"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/form-data/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/form-data/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/formidable": {
+ "version": "3.5.4",
+ "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
+ "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@paralleldrive/cuid2": "^2.2.2",
+ "dezalgo": "^1.0.4",
+ "once": "^1.4.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/tunnckoCore/commissions"
+ }
+ },
+ "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": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "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/generate-function": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+ "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-property": "^1.0.2"
+ }
+ },
+ "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-port-please": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz",
+ "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "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/giget": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/giget/-/giget-3.3.1.tgz",
+ "integrity": "sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==",
+ "devOptional": true,
+ "license": "MIT",
+ "bin": {
+ "giget": "dist/cli.mjs"
+ }
+ },
+ "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==",
+ "devOptional": true,
+ "license": "ISC"
+ },
+ "node_modules/grammex": {
+ "version": "3.1.13",
+ "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.13.tgz",
+ "integrity": "sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/graphmatch": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz",
+ "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "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.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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/iconv-lite": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
+ "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "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/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/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "devOptional": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/intl-messageformat": {
+ "version": "10.7.18",
+ "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz",
+ "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "2.3.6",
+ "@formatjs/fast-memoize": "2.2.7",
+ "@formatjs/icu-messageformat-parser": "2.11.4",
+ "tslib": "^2.8.0"
+ }
+ },
+ "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-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/is-property": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/is-retry-allowed": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-3.0.0.tgz",
+ "integrity": "sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "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==",
+ "devOptional": true,
+ "license": "ISC"
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "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==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^4.0.1",
+ "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": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "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": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "devOptional": true,
+ "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.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.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/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/lru.min": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.5.tgz",
+ "integrity": "sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.0.0",
+ "deno": ">=1.30.0",
+ "node": ">=8.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wellwelwel"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.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": "1.1.1",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
+ "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "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/mysql2": {
+ "version": "3.24.4",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.24.4.tgz",
+ "integrity": "sha512-A2olluVlj0mvgyIRRISMEzXc51m+21mRtcMVjJyIpt2GG98+XrC9m9HzsqcMsX2LcnfccJvY5NB22g8fENBnOA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "aws-ssl-profiles": "^1.1.2",
+ "generate-function": "^2.3.1",
+ "iconv-lite": "^0.7.3",
+ "long": "^5.3.2",
+ "lru.min": "^1.1.4",
+ "named-placeholders": "^1.1.6",
+ "sql-escaper": "^1.5.1"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">= 8"
+ }
+ },
+ "node_modules/named-placeholders": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
+ "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "lru.min": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.19",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
+ "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",
+ "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/negotiator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz",
+ "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/negotiator/node_modules/content-type": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "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/obug": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
+ "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/ohash": {
+ "version": "2.0.12",
+ "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz",
+ "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/on-exit-leak-free": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
+ "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "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/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/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "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/perfect-debounce": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
+ "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/pg": {
+ "version": "8.23.1",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz",
+ "integrity": "sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.14.1",
+ "pg-pool": "^3.14.0",
+ "pg-protocol": "^1.16.1",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.4.1"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.1.tgz",
+ "integrity": "sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.14.1",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.1.tgz",
+ "integrity": "sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
+ "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.1.tgz",
+ "integrity": "sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
+ "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.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pino": {
+ "version": "9.14.0",
+ "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz",
+ "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==",
+ "license": "MIT",
+ "dependencies": {
+ "@pinojs/redact": "^0.4.0",
+ "atomic-sleep": "^1.0.0",
+ "on-exit-leak-free": "^2.1.0",
+ "pino-abstract-transport": "^2.0.0",
+ "pino-std-serializers": "^7.0.0",
+ "process-warning": "^5.0.0",
+ "quick-format-unescaped": "^4.0.3",
+ "real-require": "^0.2.0",
+ "safe-stable-stringify": "^2.3.1",
+ "sonic-boom": "^4.0.1",
+ "thread-stream": "^3.0.0"
+ },
+ "bin": {
+ "pino": "bin.js"
+ }
+ },
+ "node_modules/pino-abstract-transport": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
+ "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.0.0"
+ }
+ },
+ "node_modules/pino-std-serializers": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
+ "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
+ "license": "MIT"
+ },
+ "node_modules/pkg-types": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.3.tgz",
+ "integrity": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "confbox": "^0.3.1",
+ "exsolve": "^1.1.1",
+ "pathe": "^2.0.3"
+ }
+ },
+ "node_modules/pkg-types/node_modules/confbox": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz",
+ "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.28",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
+ "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
+ "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.18",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postgres": {
+ "version": "3.4.7",
+ "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz",
+ "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==",
+ "devOptional": true,
+ "license": "Unlicense",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/sponsors/porsager"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/prisma": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.10.1.tgz",
+ "integrity": "sha512-GoVeErHGGtsSjsflQ1r2BA3RKTXtxNraQJz8OLkLhoLr7OgCscdx4qw4WMM4HnERYc+a1cq2VnMNbep+vOjNcQ==",
+ "devOptional": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/config": "7.10.1",
+ "@prisma/dev": "0.24.17",
+ "@prisma/engines": "7.10.1",
+ "@prisma/studio-core": "0.33.0",
+ "mysql2": "3.24.4",
+ "postgres": "3.4.7"
+ },
+ "bin": {
+ "prisma": "build/index.js"
+ },
+ "engines": {
+ "node": "^20.19 || ^22.12 || >=24.0"
+ },
+ "peerDependencies": {
+ "better-sqlite3": ">=9.0.0",
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "better-sqlite3": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/process-warning": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.1.tgz",
+ "integrity": "sha512-oCkNVj4OKnKEcocVBcDfiIugN1/cE6rgfT3nCtzeqZPA8+rIxMaQ0xXzIkGP8Gj2qUaP1UR7uhJ1VD8lUCpzCg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/proper-lockfile": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
+ "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "retry": "^0.12.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "node_modules/proper-lockfile/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==",
+ "devOptional": true,
+ "license": "ISC"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
+ "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "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/qs": {
+ "version": "6.16.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
+ "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "es-define-property": "^1.0.1",
+ "side-channel": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/quick-format-unescaped": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
+ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
+ "license": "MIT"
+ },
+ "node_modules/range-parser": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
+ "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/rc9": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.1.0.tgz",
+ "integrity": "sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "defu": "^6.1.7",
+ "destr": "^2.0.5"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz",
+ "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==",
+ "devOptional": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz",
+ "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "scheduler": "^0.28.0"
+ },
+ "peerDependencies": {
+ "react": "^19.3.1"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz",
+ "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/real-require": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
+ "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12.13.0"
+ }
+ },
+ "node_modules/remeda": {
+ "version": "2.33.4",
+ "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz",
+ "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/remeda"
+ }
+ },
+ "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==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ret": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz",
+ "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/robust-predicates": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
+ "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
+ "devOptional": true,
+ "license": "Unlicense"
+ },
+ "node_modules/rollup": {
+ "version": "4.63.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.2.tgz",
+ "integrity": "sha512-l5eyksV4tPBj6lJyEa37YzIOCSOV7lkZzEHUdpjWZbtD7wTcFYmEYXSgm5bT4vV+dZLb9rBG1W9GROOG4NS4Ew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
+ "@rollup/rollup-android-arm-eabi": "4.63.2",
+ "@rollup/rollup-android-arm64": "4.63.2",
+ "@rollup/rollup-darwin-arm64": "4.63.2",
+ "@rollup/rollup-darwin-x64": "4.63.2",
+ "@rollup/rollup-freebsd-arm64": "4.63.2",
+ "@rollup/rollup-freebsd-x64": "4.63.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.63.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.63.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.63.2",
+ "@rollup/rollup-linux-arm64-musl": "4.63.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.63.2",
+ "@rollup/rollup-linux-loong64-musl": "4.63.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.63.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.63.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.63.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.63.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.63.2",
+ "@rollup/rollup-linux-x64-gnu": "4.63.2",
+ "@rollup/rollup-linux-x64-musl": "4.63.2",
+ "@rollup/rollup-openbsd-x64": "4.63.2",
+ "@rollup/rollup-openharmony-arm64": "4.63.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.63.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.63.2",
+ "@rollup/rollup-win32-x64-gnu": "4.63.2",
+ "@rollup/rollup-win32-x64-msvc": "4.63.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "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/safe-regex2": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.2.tgz",
+ "integrity": "sha512-tAtCGFPPP02VroDVzp4daGi6bC7jWG+41D47GKf18FeH/ubvKQHT9VmhAkvtjNOmJwqtApzWeUpEhkyEYULvmw==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "ret": "~0.5.0"
+ },
+ "bin": {
+ "safe-regex2": "bin/safe-regex2.js"
+ }
+ },
+ "node_modules/safe-stable-stringify": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
+ "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "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/scheduler": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
+ "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
+ "devOptional": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "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==",
+ "devOptional": 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==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
+ "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.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "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/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "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==",
+ "devOptional": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/smol-toml": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz",
+ "integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/cyyynthia"
+ }
+ },
+ "node_modules/sonic-boom": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
+ "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "atomic-sleep": "^1.0.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
+ "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/sql-escaper": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.5.2.tgz",
+ "integrity": "sha512-6CKD38c31SENivxOADeMNLdukOnUxUcflKtzVWzace7Riv1v7cAEym5Cx9Q7gYZ3ezIJI7ZpqecQq8cqNeSSFg==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.0.0",
+ "deno": ">=2.0.0",
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "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/std-env": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
+ "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/superagent": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz",
+ "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "component-emitter": "^1.3.1",
+ "cookiejar": "^2.1.4",
+ "debug": "^4.3.7",
+ "fast-safe-stringify": "^2.1.1",
+ "form-data": "^4.0.5",
+ "formidable": "^3.5.4",
+ "methods": "^1.1.2",
+ "mime": "2.6.0",
+ "qs": "^6.14.1"
+ },
+ "engines": {
+ "node": ">=14.18.0"
+ }
+ },
+ "node_modules/supertest": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz",
+ "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cookie-signature": "^1.2.2",
+ "methods": "^1.1.2",
+ "superagent": "^10.3.0"
+ },
+ "engines": {
+ "node": ">=14.18.0"
+ }
+ },
+ "node_modules/thread-stream": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz",
+ "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==",
+ "license": "MIT",
+ "dependencies": {
+ "real-require": "^0.2.0"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
+ "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
+ "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.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/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/tsx": {
+ "version": "4.23.13",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz",
+ "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.28.0"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
+ "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^2.0.0",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/type-is/node_modules/content-type": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
+ "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc"
+ },
+ "engines": {
+ "node": ">=16.20.0"
+ },
+ "optionalDependencies": {
+ "@typescript/typescript-aix-ppc64": "7.0.2",
+ "@typescript/typescript-darwin-arm64": "7.0.2",
+ "@typescript/typescript-darwin-x64": "7.0.2",
+ "@typescript/typescript-freebsd-arm64": "7.0.2",
+ "@typescript/typescript-freebsd-x64": "7.0.2",
+ "@typescript/typescript-linux-arm": "7.0.2",
+ "@typescript/typescript-linux-arm64": "7.0.2",
+ "@typescript/typescript-linux-loong64": "7.0.2",
+ "@typescript/typescript-linux-mips64el": "7.0.2",
+ "@typescript/typescript-linux-ppc64": "7.0.2",
+ "@typescript/typescript-linux-riscv64": "7.0.2",
+ "@typescript/typescript-linux-s390x": "7.0.2",
+ "@typescript/typescript-linux-x64": "7.0.2",
+ "@typescript/typescript-netbsd-arm64": "7.0.2",
+ "@typescript/typescript-netbsd-x64": "7.0.2",
+ "@typescript/typescript-openbsd-arm64": "7.0.2",
+ "@typescript/typescript-openbsd-x64": "7.0.2",
+ "@typescript/typescript-sunos-x64": "7.0.2",
+ "@typescript/typescript-win32-arm64": "7.0.2",
+ "@typescript/typescript-win32-x64": "7.0.2"
+ }
+ },
+ "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": "8.9.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
+ "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
+ "license": "MIT"
+ },
+ "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/valibot": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz",
+ "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "typescript": ">=5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "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/vite": {
+ "version": "6.4.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
+ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
+ "dev": true,
+ "license": "MIT",
+ "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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vitest": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz",
+ "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.11",
+ "@vitest/mocker": "4.1.11",
+ "@vitest/pretty-format": "4.1.11",
+ "@vitest/runner": "4.1.11",
+ "@vitest/snapshot": "4.1.11",
+ "@vitest/spy": "4.1.11",
+ "@vitest/utils": "4.1.11",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.11",
+ "@vitest/browser-preview": "4.1.11",
+ "@vitest/browser-webdriverio": "4.1.11",
+ "@vitest/coverage-istanbul": "4.1.11",
+ "@vitest/coverage-v8": "4.1.11",
+ "@vitest/ui": "4.1.11",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/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/zeptomatch": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz",
+ "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "grammex": "^3.1.11",
+ "graphmatch": "^1.1.0"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.6.5",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz",
+ "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/backend/package.json b/backend/package.json
index 8ba2b9a0..8a21cd49 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,57 +1,57 @@
-{
- "name": "docforum-core-backend",
- "version": "0.0.0",
- "private": true,
- "description": "DocForum backend β modular monolith. See ARCHITECTURE0.md.",
- "engines": {
- "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
- },
- "scripts": {
- "dev": "tsx watch src/server.ts",
- "build": "tsc -p tsconfig.build.json",
- "start": "node dist/server.js",
- "typecheck": "tsc --noEmit",
- "test": "vitest run tests/unit",
- "test:integration": "vitest run tests/integration",
- "test:integration:payments-testnet": "DATABASE_URL_TEST==postgresql://docforum:docforum@localhost:5435/docforum_payments_test vitest run tests/testnet-integration",
- "test:all": "vitest run",
- "test:properties": "vitest run tests/unit/availability-slots.property.test.ts tests/unit/state-machines.property.test.ts",
- "audit:prod": "node scripts/audit-gate.mjs",
- "i18n:check": "node scripts/check-i18n-catalogues.mjs",
- "prisma:generate": "prisma generate",
- "prisma:migrate": "prisma migrate dev"
- },
- "dependencies": {
- "@docforum/escrow-sdk": "https://github.com/DocForum/docforum-escrow/releases/download/sdk-v0.2.0/docforum-escrow-sdk-0.2.0.tgz",
- "@prisma/adapter-pg": "^7.10.1",
- "@prisma/client": "^7.10.1",
- "@sendgrid/mail": "^8.1.6",
- "@stellar/stellar-sdk": "^16.0.1",
- "bcryptjs": "^3.0.3",
- "cookie-parser": "^1.4.7",
- "cors": "^2.8.6",
- "dotenv": "^17.4.2",
- "express": "^5.2.1",
- "intl-messageformat": "^10.7.18",
- "jsonwebtoken": "^9.0.3",
- "pg": "^8.23.1",
- "pino": "^9.13.0",
- "zod": "^4.6.5"
- },
- "devDependencies": {
- "@types/bcryptjs": "^2.4.6",
- "@types/cookie-parser": "^1.4.10",
- "@types/cors": "^2.8.19",
- "@types/express": "^5.0.6",
- "@types/jsonwebtoken": "^9.0.10",
- "@types/node": "^26.5.1",
- "@types/supertest": "^7.2.1",
- "embedded-postgres": "^18.4.0-beta.17",
- "fast-check": "^3.23.2",
- "prisma": "^7.10.1",
- "supertest": "^7.2.2",
- "tsx": "^4.23.13",
- "typescript": "^7.0.2",
- "vitest": "^4.1.11"
- }
-}
\ No newline at end of file
+{
+ "name": "docforum-core-backend",
+ "version": "0.0.0",
+ "private": true,
+ "description": "DocForum backend \u2014 modular monolith. See ARCHITECTURE0.md.",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "scripts": {
+ "dev": "node init && tsx watch src/server.ts",
+ "build": "node init && tsc -p tsconfig.build.json",
+ "start": "node init && node dist/server.js",
+ "typecheck": "tsc --noEmit",
+ "test": "vitest run tests/unit",
+ "test:integration": "vitest run tests/integration",
+ "test:integration:payments-testnet": "DATABASE_URL_TEST==postgresql://docforum:docforum@localhost:5435/docforum_payments_test vitest run tests/testnet-integration",
+ "test:all": "vitest run",
+ "test:properties": "vitest run tests/unit/availability-slots.property.test.ts tests/unit/state-machines.property.test.ts",
+ "audit:prod": "node scripts/audit-gate.mjs",
+ "i18n:check": "node scripts/check-i18n-catalogues.mjs",
+ "prisma:generate": "prisma generate",
+ "prisma:migrate": "prisma migrate dev"
+ },
+ "dependencies": {
+ "@docforum/escrow-sdk": "https://github.com/DocForum/docforum-escrow/releases/download/sdk-v0.2.0/docforum-escrow-sdk-0.2.0.tgz",
+ "@prisma/adapter-pg": "^7.10.1",
+ "@prisma/client": "^7.10.1",
+ "@sendgrid/mail": "^8.1.6",
+ "@stellar/stellar-sdk": "^16.0.1",
+ "bcryptjs": "^3.0.3",
+ "cookie-parser": "^1.4.7",
+ "cors": "^2.8.6",
+ "dotenv": "^17.4.2",
+ "express": "^5.2.1",
+ "intl-messageformat": "^10.7.18",
+ "jsonwebtoken": "^9.0.3",
+ "pg": "^8.23.1",
+ "pino": "^9.13.0",
+ "zod": "^4.6.5"
+ },
+ "devDependencies": {
+ "@types/bcryptjs": "^2.4.6",
+ "@types/cookie-parser": "^1.4.10",
+ "@types/cors": "^2.8.19",
+ "@types/express": "^5.0.6",
+ "@types/jsonwebtoken": "^9.0.10",
+ "@types/node": "^26.5.1",
+ "@types/supertest": "^7.2.1",
+ "embedded-postgres": "^18.4.0-beta.17",
+ "fast-check": "^3.23.2",
+ "prisma": "^7.10.1",
+ "supertest": "^7.2.2",
+ "tsx": "^4.23.13",
+ "typescript": "^7.0.2",
+ "vitest": "^4.1.11"
+ }
+}
diff --git a/backend/prisma/migrations/20261015175723_add_password_reset_tokens/migration.sql b/backend/prisma/migrations/20261015175723_add_password_reset_tokens/migration.sql
index 094bc27a..a0cbd651 100644
--- a/backend/prisma/migrations/20261015175723_add_password_reset_tokens/migration.sql
+++ b/backend/prisma/migrations/20261015175723_add_password_reset_tokens/migration.sql
@@ -1,23 +1,23 @@
--- AlterTable
-ALTER TABLE "User" ADD COLUMN "tokenVersion" INTEGER NOT NULL DEFAULT 0;
-
--- CreateTable
-CREATE TABLE "PasswordResetToken" (
- "id" TEXT NOT NULL,
- "userId" TEXT NOT NULL,
- "tokenHash" TEXT NOT NULL,
- "expiresAt" TIMESTAMP(3) NOT NULL,
- "usedAt" TIMESTAMP(3),
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-
- CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE UNIQUE INDEX "PasswordResetToken_tokenHash_key" ON "PasswordResetToken"("tokenHash");
-
--- CreateIndex
-CREATE INDEX "PasswordResetToken_userId_idx" ON "PasswordResetToken"("userId");
-
--- AddForeignKey
-ALTER TABLE "PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN "tokenVersion" INTEGER NOT NULL DEFAULT 0;
+
+-- CreateTable
+CREATE TABLE "PasswordResetToken" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "tokenHash" TEXT NOT NULL,
+ "expiresAt" TIMESTAMP(3) NOT NULL,
+ "usedAt" TIMESTAMP(3),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "PasswordResetToken_tokenHash_key" ON "PasswordResetToken"("tokenHash");
+
+-- CreateIndex
+CREATE INDEX "PasswordResetToken_userId_idx" ON "PasswordResetToken"("userId");
+
+-- AddForeignKey
+ALTER TABLE "PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/backend/prisma/migrations/20261015182442_add_refresh_sessions/migration.sql b/backend/prisma/migrations/20261015182442_add_refresh_sessions/migration.sql
index e037fc45..70f7c542 100644
--- a/backend/prisma/migrations/20261015182442_add_refresh_sessions/migration.sql
+++ b/backend/prisma/migrations/20261015182442_add_refresh_sessions/migration.sql
@@ -1,21 +1,21 @@
--- CreateTable
-CREATE TABLE "RefreshSession" (
- "id" TEXT NOT NULL,
- "userId" TEXT NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "lastUsedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "revokedAt" TIMESTAMP(3),
- "userAgent" TEXT,
- "ip" TEXT,
-
- CONSTRAINT "RefreshSession_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE INDEX "RefreshSession_userId_idx" ON "RefreshSession"("userId");
-
--- CreateIndex
-CREATE INDEX "RefreshSession_userId_revokedAt_idx" ON "RefreshSession"("userId", "revokedAt");
-
--- AddForeignKey
-ALTER TABLE "RefreshSession" ADD CONSTRAINT "RefreshSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+-- CreateTable
+CREATE TABLE "RefreshSession" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "lastUsedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "revokedAt" TIMESTAMP(3),
+ "userAgent" TEXT,
+ "ip" TEXT,
+
+ CONSTRAINT "RefreshSession_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "RefreshSession_userId_idx" ON "RefreshSession"("userId");
+
+-- CreateIndex
+CREATE INDEX "RefreshSession_userId_revokedAt_idx" ON "RefreshSession"("userId", "revokedAt");
+
+-- AddForeignKey
+ALTER TABLE "RefreshSession" ADD CONSTRAINT "RefreshSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/backend/prisma/migrations/20261016130509_add_sep10_challenges/migration.sql b/backend/prisma/migrations/20261016130509_add_sep10_challenges/migration.sql
index b222c397..06974b7d 100644
--- a/backend/prisma/migrations/20261016130509_add_sep10_challenges/migration.sql
+++ b/backend/prisma/migrations/20261016130509_add_sep10_challenges/migration.sql
@@ -1,22 +1,22 @@
--- CreateTable
-CREATE TABLE "Sep10Challenge" (
- "id" TEXT NOT NULL,
- "userId" TEXT NOT NULL,
- "account" TEXT NOT NULL,
- "nonce" TEXT NOT NULL,
- "expiresAt" TIMESTAMP(3) NOT NULL,
- "consumedAt" TIMESTAMP(3),
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-
- CONSTRAINT "Sep10Challenge_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE UNIQUE INDEX "Sep10Challenge_nonce_key" ON "Sep10Challenge"("nonce");
-
--- CreateIndex
-CREATE INDEX "Sep10Challenge_expiresAt_idx" ON "Sep10Challenge"("expiresAt");
-
--- AddForeignKey
-ALTER TABLE "Sep10Challenge" ADD CONSTRAINT "Sep10Challenge_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
+-- CreateTable
+CREATE TABLE "Sep10Challenge" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "account" TEXT NOT NULL,
+ "nonce" TEXT NOT NULL,
+ "expiresAt" TIMESTAMP(3) NOT NULL,
+ "consumedAt" TIMESTAMP(3),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "Sep10Challenge_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Sep10Challenge_nonce_key" ON "Sep10Challenge"("nonce");
+
+-- CreateIndex
+CREATE INDEX "Sep10Challenge_expiresAt_idx" ON "Sep10Challenge"("expiresAt");
+
+-- AddForeignKey
+ALTER TABLE "Sep10Challenge" ADD CONSTRAINT "Sep10Challenge_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
diff --git a/backend/prisma/migrations/20261019100000_payment_assets_and_prepare/migration.sql b/backend/prisma/migrations/20261019100000_payment_assets_and_prepare/migration.sql
index f6cd3f70..6494c743 100644
--- a/backend/prisma/migrations/20261019100000_payment_assets_and_prepare/migration.sql
+++ b/backend/prisma/migrations/20261019100000_payment_assets_and_prepare/migration.sql
@@ -1,42 +1,42 @@
--- Issue #79: PaymentIntent is denominated in an allowlisted asset, not
--- implicitly native XLM. The amount column is renamed, not recreated, so
--- every existing row keeps its exact smallest-unit value (no data copy, no
--- precision loss). `assetCode` is backfilled to 'XLM' because that is the
--- only asset v1 could have created; `tokenContractId` stays NULL for those
--- rows and the service resolves it from ESCROW_TOKEN_ID (see schema comment).
-ALTER TABLE "PaymentIntent" RENAME COLUMN "amountStroops" TO "amount";
-ALTER TABLE "PaymentIntent" ADD COLUMN "assetCode" TEXT NOT NULL DEFAULT 'XLM';
-ALTER TABLE "PaymentIntent" ADD COLUMN "tokenContractId" TEXT;
-ALTER TABLE "PaymentIntent" ALTER COLUMN "assetCode" DROP DEFAULT;
-
--- Issue #80: prepare/expire state machine. `awaiting_signature` and
--- `submitting` are added to the status enum (submitting is reserved for the
--- submit half of ADR 0005 and not written yet).
-ALTER TYPE "PaymentIntentStatus" ADD VALUE 'awaiting_signature';
-ALTER TYPE "PaymentIntentStatus" ADD VALUE 'submitting';
-
-ALTER TABLE "PaymentIntent" ADD COLUMN "preparedTxHash" TEXT;
-ALTER TABLE "PaymentIntent" ADD COLUMN "preparedXdr" TEXT;
-ALTER TABLE "PaymentIntent" ADD COLUMN "preparedExpiryLedger" INTEGER;
-ALTER TABLE "PaymentIntent" ADD COLUMN "preparedExpiresAt" TIMESTAMP(3);
-ALTER TABLE "PaymentIntent" ADD COLUMN "refundAfter" TIMESTAMP(3);
-
--- Expiry sweep: find live prepared transactions whose window has passed.
-CREATE INDEX "PaymentIntent_status_preparedExpiresAt_idx" ON "PaymentIntent"("status", "preparedExpiresAt");
-
--- ADR 0005 step 1: patient payer wallet link (SEP-10 verified once that
--- flow exists). Separate from WalletLink, which stays the facility payee.
-CREATE TABLE "PatientWalletLink" (
- "id" TEXT NOT NULL,
- "patientProfileId" TEXT NOT NULL,
- "stellarPublicKey" TEXT NOT NULL,
- "verifiedAt" TIMESTAMP(3) NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "updatedAt" TIMESTAMP(3) NOT NULL,
-
- CONSTRAINT "PatientWalletLink_pkey" PRIMARY KEY ("id")
-);
-
-CREATE UNIQUE INDEX "PatientWalletLink_patientProfileId_key" ON "PatientWalletLink"("patientProfileId");
-
-ALTER TABLE "PatientWalletLink" ADD CONSTRAINT "PatientWalletLink_patientProfileId_fkey" FOREIGN KEY ("patientProfileId") REFERENCES "PatientProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+-- Issue #79: PaymentIntent is denominated in an allowlisted asset, not
+-- implicitly native XLM. The amount column is renamed, not recreated, so
+-- every existing row keeps its exact smallest-unit value (no data copy, no
+-- precision loss). `assetCode` is backfilled to 'XLM' because that is the
+-- only asset v1 could have created; `tokenContractId` stays NULL for those
+-- rows and the service resolves it from ESCROW_TOKEN_ID (see schema comment).
+ALTER TABLE "PaymentIntent" RENAME COLUMN "amountStroops" TO "amount";
+ALTER TABLE "PaymentIntent" ADD COLUMN "assetCode" TEXT NOT NULL DEFAULT 'XLM';
+ALTER TABLE "PaymentIntent" ADD COLUMN "tokenContractId" TEXT;
+ALTER TABLE "PaymentIntent" ALTER COLUMN "assetCode" DROP DEFAULT;
+
+-- Issue #80: prepare/expire state machine. `awaiting_signature` and
+-- `submitting` are added to the status enum (submitting is reserved for the
+-- submit half of ADR 0005 and not written yet).
+ALTER TYPE "PaymentIntentStatus" ADD VALUE 'awaiting_signature';
+ALTER TYPE "PaymentIntentStatus" ADD VALUE 'submitting';
+
+ALTER TABLE "PaymentIntent" ADD COLUMN "preparedTxHash" TEXT;
+ALTER TABLE "PaymentIntent" ADD COLUMN "preparedXdr" TEXT;
+ALTER TABLE "PaymentIntent" ADD COLUMN "preparedExpiryLedger" INTEGER;
+ALTER TABLE "PaymentIntent" ADD COLUMN "preparedExpiresAt" TIMESTAMP(3);
+ALTER TABLE "PaymentIntent" ADD COLUMN "refundAfter" TIMESTAMP(3);
+
+-- Expiry sweep: find live prepared transactions whose window has passed.
+CREATE INDEX "PaymentIntent_status_preparedExpiresAt_idx" ON "PaymentIntent"("status", "preparedExpiresAt");
+
+-- ADR 0005 step 1: patient payer wallet link (SEP-10 verified once that
+-- flow exists). Separate from WalletLink, which stays the facility payee.
+CREATE TABLE "PatientWalletLink" (
+ "id" TEXT NOT NULL,
+ "patientProfileId" TEXT NOT NULL,
+ "stellarPublicKey" TEXT NOT NULL,
+ "verifiedAt" TIMESTAMP(3) NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "PatientWalletLink_pkey" PRIMARY KEY ("id")
+);
+
+CREATE UNIQUE INDEX "PatientWalletLink_patientProfileId_key" ON "PatientWalletLink"("patientProfileId");
+
+ALTER TABLE "PatientWalletLink" ADD CONSTRAINT "PatientWalletLink_patientProfileId_fkey" FOREIGN KEY ("patientProfileId") REFERENCES "PatientProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/backend/prisma/migrations/20261020103435_add_fulfillment_proof_and_disputes/migration.sql b/backend/prisma/migrations/20261020103435_add_fulfillment_proof_and_disputes/migration.sql
index da812943..4046fde9 100644
--- a/backend/prisma/migrations/20261020103435_add_fulfillment_proof_and_disputes/migration.sql
+++ b/backend/prisma/migrations/20261020103435_add_fulfillment_proof_and_disputes/migration.sql
@@ -1,118 +1,118 @@
--- CreateEnum
-CREATE TYPE "OrderType" AS ENUM ('pharmacy', 'lab');
-
--- CreateEnum
-CREATE TYPE "DisputeStatus" AS ENUM ('pending', 'resolved');
-
--- CreateTable
-CREATE TABLE "Order" (
- "id" TEXT NOT NULL,
- "orderType" "OrderType" NOT NULL,
- "patientProfileId" TEXT NOT NULL,
- "facilityProfileId" TEXT,
- "prescribingDoctorId" TEXT,
- "details" TEXT,
- "status" "OrderStatus" NOT NULL DEFAULT 'issued',
- "fulfilledAt" TIMESTAMP(3),
- "confirmed" BOOLEAN NOT NULL DEFAULT false,
- "confirmationSource" TEXT,
- "confirmedAt" TIMESTAMP(3),
- "disputed" BOOLEAN NOT NULL DEFAULT false,
- "disputedAt" TIMESTAMP(3),
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "updatedAt" TIMESTAMP(3) NOT NULL,
-
- CONSTRAINT "Order_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "FulfillmentProof" (
- "id" TEXT NOT NULL,
- "orderId" TEXT NOT NULL,
- "facilityProfileId" TEXT NOT NULL,
- "referenceNumber" TEXT NOT NULL,
- "attachmentUrl" TEXT,
- "notes" TEXT,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-
- CONSTRAINT "FulfillmentProof_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "OrderDispute" (
- "id" TEXT NOT NULL,
- "orderId" TEXT NOT NULL,
- "patientProfileId" TEXT NOT NULL,
- "reason" TEXT NOT NULL,
- "status" "DisputeStatus" NOT NULL DEFAULT 'pending',
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "updatedAt" TIMESTAMP(3) NOT NULL,
-
- CONSTRAINT "OrderDispute_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "OrderDisputeResolution" (
- "id" TEXT NOT NULL,
- "disputeId" TEXT NOT NULL,
- "adminUserId" TEXT NOT NULL,
- "resolutionNotes" TEXT NOT NULL,
- "outcome" TEXT NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-
- CONSTRAINT "OrderDisputeResolution_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE INDEX "Order_patientProfileId_status_idx" ON "Order"("patientProfileId", "status");
-
--- CreateIndex
-CREATE INDEX "Order_facilityProfileId_status_idx" ON "Order"("facilityProfileId", "status");
-
--- CreateIndex
-CREATE INDEX "Order_status_confirmed_disputed_idx" ON "Order"("status", "confirmed", "disputed");
-
--- CreateIndex
-CREATE INDEX "FulfillmentProof_orderId_idx" ON "FulfillmentProof"("orderId");
-
--- CreateIndex
-CREATE INDEX "FulfillmentProof_facilityProfileId_idx" ON "FulfillmentProof"("facilityProfileId");
-
--- CreateIndex
-CREATE INDEX "OrderDispute_orderId_idx" ON "OrderDispute"("orderId");
-
--- CreateIndex
-CREATE INDEX "OrderDispute_patientProfileId_idx" ON "OrderDispute"("patientProfileId");
-
--- CreateIndex
-CREATE INDEX "OrderDispute_status_idx" ON "OrderDispute"("status");
-
--- CreateIndex
-CREATE INDEX "OrderDisputeResolution_disputeId_idx" ON "OrderDisputeResolution"("disputeId");
-
--- CreateIndex
-CREATE INDEX "OrderDisputeResolution_adminUserId_idx" ON "OrderDisputeResolution"("adminUserId");
-
--- AddForeignKey
-ALTER TABLE "Order" ADD CONSTRAINT "Order_patientProfileId_fkey" FOREIGN KEY ("patientProfileId") REFERENCES "PatientProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "Order" ADD CONSTRAINT "Order_facilityProfileId_fkey" FOREIGN KEY ("facilityProfileId") REFERENCES "FacilityProfile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "FulfillmentProof" ADD CONSTRAINT "FulfillmentProof_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "FulfillmentProof" ADD CONSTRAINT "FulfillmentProof_facilityProfileId_fkey" FOREIGN KEY ("facilityProfileId") REFERENCES "FacilityProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "OrderDispute" ADD CONSTRAINT "OrderDispute_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "OrderDispute" ADD CONSTRAINT "OrderDispute_patientProfileId_fkey" FOREIGN KEY ("patientProfileId") REFERENCES "PatientProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "OrderDisputeResolution" ADD CONSTRAINT "OrderDisputeResolution_disputeId_fkey" FOREIGN KEY ("disputeId") REFERENCES "OrderDispute"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "OrderDisputeResolution" ADD CONSTRAINT "OrderDisputeResolution_adminUserId_fkey" FOREIGN KEY ("adminUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+-- CreateEnum
+CREATE TYPE "OrderType" AS ENUM ('pharmacy', 'lab');
+
+-- CreateEnum
+CREATE TYPE "DisputeStatus" AS ENUM ('pending', 'resolved');
+
+-- CreateTable
+CREATE TABLE "Order" (
+ "id" TEXT NOT NULL,
+ "orderType" "OrderType" NOT NULL,
+ "patientProfileId" TEXT NOT NULL,
+ "facilityProfileId" TEXT,
+ "prescribingDoctorId" TEXT,
+ "details" TEXT,
+ "status" "OrderStatus" NOT NULL DEFAULT 'issued',
+ "fulfilledAt" TIMESTAMP(3),
+ "confirmed" BOOLEAN NOT NULL DEFAULT false,
+ "confirmationSource" TEXT,
+ "confirmedAt" TIMESTAMP(3),
+ "disputed" BOOLEAN NOT NULL DEFAULT false,
+ "disputedAt" TIMESTAMP(3),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Order_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "FulfillmentProof" (
+ "id" TEXT NOT NULL,
+ "orderId" TEXT NOT NULL,
+ "facilityProfileId" TEXT NOT NULL,
+ "referenceNumber" TEXT NOT NULL,
+ "attachmentUrl" TEXT,
+ "notes" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "FulfillmentProof_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "OrderDispute" (
+ "id" TEXT NOT NULL,
+ "orderId" TEXT NOT NULL,
+ "patientProfileId" TEXT NOT NULL,
+ "reason" TEXT NOT NULL,
+ "status" "DisputeStatus" NOT NULL DEFAULT 'pending',
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "OrderDispute_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "OrderDisputeResolution" (
+ "id" TEXT NOT NULL,
+ "disputeId" TEXT NOT NULL,
+ "adminUserId" TEXT NOT NULL,
+ "resolutionNotes" TEXT NOT NULL,
+ "outcome" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "OrderDisputeResolution_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "Order_patientProfileId_status_idx" ON "Order"("patientProfileId", "status");
+
+-- CreateIndex
+CREATE INDEX "Order_facilityProfileId_status_idx" ON "Order"("facilityProfileId", "status");
+
+-- CreateIndex
+CREATE INDEX "Order_status_confirmed_disputed_idx" ON "Order"("status", "confirmed", "disputed");
+
+-- CreateIndex
+CREATE INDEX "FulfillmentProof_orderId_idx" ON "FulfillmentProof"("orderId");
+
+-- CreateIndex
+CREATE INDEX "FulfillmentProof_facilityProfileId_idx" ON "FulfillmentProof"("facilityProfileId");
+
+-- CreateIndex
+CREATE INDEX "OrderDispute_orderId_idx" ON "OrderDispute"("orderId");
+
+-- CreateIndex
+CREATE INDEX "OrderDispute_patientProfileId_idx" ON "OrderDispute"("patientProfileId");
+
+-- CreateIndex
+CREATE INDEX "OrderDispute_status_idx" ON "OrderDispute"("status");
+
+-- CreateIndex
+CREATE INDEX "OrderDisputeResolution_disputeId_idx" ON "OrderDisputeResolution"("disputeId");
+
+-- CreateIndex
+CREATE INDEX "OrderDisputeResolution_adminUserId_idx" ON "OrderDisputeResolution"("adminUserId");
+
+-- AddForeignKey
+ALTER TABLE "Order" ADD CONSTRAINT "Order_patientProfileId_fkey" FOREIGN KEY ("patientProfileId") REFERENCES "PatientProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Order" ADD CONSTRAINT "Order_facilityProfileId_fkey" FOREIGN KEY ("facilityProfileId") REFERENCES "FacilityProfile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "FulfillmentProof" ADD CONSTRAINT "FulfillmentProof_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "FulfillmentProof" ADD CONSTRAINT "FulfillmentProof_facilityProfileId_fkey" FOREIGN KEY ("facilityProfileId") REFERENCES "FacilityProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OrderDispute" ADD CONSTRAINT "OrderDispute_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OrderDispute" ADD CONSTRAINT "OrderDispute_patientProfileId_fkey" FOREIGN KEY ("patientProfileId") REFERENCES "PatientProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OrderDisputeResolution" ADD CONSTRAINT "OrderDisputeResolution_disputeId_fkey" FOREIGN KEY ("disputeId") REFERENCES "OrderDispute"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OrderDisputeResolution" ADD CONSTRAINT "OrderDisputeResolution_adminUserId_fkey" FOREIGN KEY ("adminUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/backend/prisma/migrations/20261020150000_add_facility_prices/migration.sql b/backend/prisma/migrations/20261020150000_add_facility_prices/migration.sql
index 1a5c313b..2805310e 100644
--- a/backend/prisma/migrations/20261020150000_add_facility_prices/migration.sql
+++ b/backend/prisma/migrations/20261020150000_add_facility_prices/migration.sql
@@ -1,35 +1,35 @@
--- Issue #127: facility price lists used to price PaymentIntents.
---
--- Versioned (append-only) prices: a price change is a new row with a later
--- "effectiveFrom", so historical intents keep their original amount and the
--- list stays auditable. Amounts are stroops (BIGINT), matching PaymentIntent.
-
-CREATE TABLE "FacilityPrice" (
- "id" TEXT NOT NULL,
- "facilityId" TEXT NOT NULL,
- "catalogItemCode" TEXT NOT NULL,
- "asset" TEXT NOT NULL,
- "amount" BIGINT NOT NULL,
- "effectiveFrom" TIMESTAMP(3) NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-
- CONSTRAINT "FacilityPrice_pkey" PRIMARY KEY ("id")
-);
-
--- Two versions of the same item/asset cannot share an effective instant.
-CREATE UNIQUE INDEX "FacilityPrice_facilityId_catalogItemCode_asset_effectiveFrom_key"
- ON "FacilityPrice"("facilityId", "catalogItemCode", "asset", "effectiveFrom");
-
--- Current-price lookup for one facility.
-CREATE INDEX "FacilityPrice_facilityId_catalogItemCode_asset_effectiveFrom_idx"
- ON "FacilityPrice"("facilityId", "catalogItemCode", "asset", "effectiveFrom");
-
--- Eligible-facility lookup: current price per facility for a catalog item.
-CREATE INDEX "FacilityPrice_catalogItemCode_asset_effectiveFrom_idx"
- ON "FacilityPrice"("catalogItemCode", "asset", "effectiveFrom");
-
-ALTER TABLE "FacilityPrice" ADD CONSTRAINT "FacilityPrice_facilityId_fkey"
- FOREIGN KEY ("facilityId") REFERENCES "FacilityProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- Which price list entry priced an intent (the amount is the snapshot).
-ALTER TABLE "PaymentIntent" ADD COLUMN "catalogItemCode" TEXT;
+-- Issue #127: facility price lists used to price PaymentIntents.
+--
+-- Versioned (append-only) prices: a price change is a new row with a later
+-- "effectiveFrom", so historical intents keep their original amount and the
+-- list stays auditable. Amounts are stroops (BIGINT), matching PaymentIntent.
+
+CREATE TABLE "FacilityPrice" (
+ "id" TEXT NOT NULL,
+ "facilityId" TEXT NOT NULL,
+ "catalogItemCode" TEXT NOT NULL,
+ "asset" TEXT NOT NULL,
+ "amount" BIGINT NOT NULL,
+ "effectiveFrom" TIMESTAMP(3) NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "FacilityPrice_pkey" PRIMARY KEY ("id")
+);
+
+-- Two versions of the same item/asset cannot share an effective instant.
+CREATE UNIQUE INDEX "FacilityPrice_facilityId_catalogItemCode_asset_effectiveFrom_key"
+ ON "FacilityPrice"("facilityId", "catalogItemCode", "asset", "effectiveFrom");
+
+-- Current-price lookup for one facility.
+CREATE INDEX "FacilityPrice_facilityId_catalogItemCode_asset_effectiveFrom_idx"
+ ON "FacilityPrice"("facilityId", "catalogItemCode", "asset", "effectiveFrom");
+
+-- Eligible-facility lookup: current price per facility for a catalog item.
+CREATE INDEX "FacilityPrice_catalogItemCode_asset_effectiveFrom_idx"
+ ON "FacilityPrice"("catalogItemCode", "asset", "effectiveFrom");
+
+ALTER TABLE "FacilityPrice" ADD CONSTRAINT "FacilityPrice_facilityId_fkey"
+ FOREIGN KEY ("facilityId") REFERENCES "FacilityProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- Which price list entry priced an intent (the amount is the snapshot).
+ALTER TABLE "PaymentIntent" ADD COLUMN "catalogItemCode" TEXT;
diff --git a/backend/prisma/migrations/20261021090000_doctor_discovery_trigram_search/migration.sql b/backend/prisma/migrations/20261021090000_doctor_discovery_trigram_search/migration.sql
index 0b1bcfcf..83c03319 100644
--- a/backend/prisma/migrations/20261021090000_doctor_discovery_trigram_search/migration.sql
+++ b/backend/prisma/migrations/20261021090000_doctor_discovery_trigram_search/migration.sql
@@ -1,13 +1,13 @@
-CREATE EXTENSION IF NOT EXISTS pg_trgm;
-
-CREATE INDEX "DoctorProfile_fullName_trgm_idx"
- ON "DoctorProfile" USING GIN ("fullName" gin_trgm_ops);
-
-CREATE INDEX "Specialty_name_trgm_idx"
- ON "Specialty" USING GIN ("name" gin_trgm_ops);
-
-INSERT INTO "Specialty" ("id", "name", "slug") VALUES
- ('00000000-0000-4000-8000-000000000023', 'Cardiology', 'cardiology'),
- ('00000000-0000-4000-8000-000000000024', 'Dermatology', 'dermatology'),
- ('00000000-0000-4000-8000-000000000025', 'Family Medicine', 'family-medicine')
-ON CONFLICT DO NOTHING;
+CREATE EXTENSION IF NOT EXISTS pg_trgm;
+
+CREATE INDEX "DoctorProfile_fullName_trgm_idx"
+ ON "DoctorProfile" USING GIN ("fullName" gin_trgm_ops);
+
+CREATE INDEX "Specialty_name_trgm_idx"
+ ON "Specialty" USING GIN ("name" gin_trgm_ops);
+
+INSERT INTO "Specialty" ("id", "name", "slug") VALUES
+ ('00000000-0000-4000-8000-000000000023', 'Cardiology', 'cardiology'),
+ ('00000000-0000-4000-8000-000000000024', 'Dermatology', 'dermatology'),
+ ('00000000-0000-4000-8000-000000000025', 'Family Medicine', 'family-medicine')
+ON CONFLICT DO NOTHING;
diff --git a/backend/prisma/migrations/20261021100000_user_locale/migration.sql b/backend/prisma/migrations/20261021100000_user_locale/migration.sql
index 4f69d63d..3bfeac95 100644
--- a/backend/prisma/migrations/20261021100000_user_locale/migration.sql
+++ b/backend/prisma/migrations/20261021100000_user_locale/migration.sql
@@ -1 +1 @@
-ALTER TABLE "User" ADD COLUMN "locale" TEXT NOT NULL DEFAULT 'en';
+ALTER TABLE "User" ADD COLUMN "locale" TEXT NOT NULL DEFAULT 'en';
diff --git a/backend/prisma/migrations/20261021120000_account_deletion_retention/migration.sql b/backend/prisma/migrations/20261021120000_account_deletion_retention/migration.sql
index 9817bfb1..29a2c3ef 100644
--- a/backend/prisma/migrations/20261021120000_account_deletion_retention/migration.sql
+++ b/backend/prisma/migrations/20261021120000_account_deletion_retention/migration.sql
@@ -1,12 +1,12 @@
--- DropForeignKey
-ALTER TABLE "PatientProfile" DROP CONSTRAINT "PatientProfile_userId_fkey";
-
--- AlterTable
-ALTER TABLE "PatientProfile" ADD COLUMN "anonymizedAt" TIMESTAMP(3);
-
--- AlterTable
-ALTER TABLE "User" ADD COLUMN "deletedAt" TIMESTAMP(3);
-
--- AddForeignKey
-ALTER TABLE "PatientProfile" ADD CONSTRAINT "PatientProfile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-
+-- DropForeignKey
+ALTER TABLE "PatientProfile" DROP CONSTRAINT "PatientProfile_userId_fkey";
+
+-- AlterTable
+ALTER TABLE "PatientProfile" ADD COLUMN "anonymizedAt" TIMESTAMP(3);
+
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN "deletedAt" TIMESTAMP(3);
+
+-- AddForeignKey
+ALTER TABLE "PatientProfile" ADD CONSTRAINT "PatientProfile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
diff --git a/backend/prisma/migrations/20261021130000_guardian_dependent_profiles/migration.sql b/backend/prisma/migrations/20261021130000_guardian_dependent_profiles/migration.sql
index c6c7002d..41661bed 100644
--- a/backend/prisma/migrations/20261021130000_guardian_dependent_profiles/migration.sql
+++ b/backend/prisma/migrations/20261021130000_guardian_dependent_profiles/migration.sql
@@ -1,46 +1,46 @@
--- Phase 8, issue #42 (docs/adr/0011): replace the unenforced
--- PatientProfile.guardianUserId string with a real Guardianship relation.
---
--- Safety: no code ever wrote guardianUserId, so every row should be NULL.
--- If any isn't, its meaning is unknown (the old field was explicitly
--- "don't build on this"), so stop here and let an operator decide rather
--- than silently dropping or reinterpreting it.
-DO $$
-BEGIN
- IF EXISTS (SELECT 1 FROM "PatientProfile" WHERE "guardianUserId" IS NOT NULL) THEN
- RAISE EXCEPTION 'PatientProfile.guardianUserId has non-null values; resolve them before applying guardian_dependent_profiles (see docs/adr/0011).';
- END IF;
-END $$;
-
--- AlterTable
-ALTER TABLE "PatientProfile" DROP COLUMN "guardianUserId",
-ALTER COLUMN "userId" DROP NOT NULL;
-
--- CreateTable
-CREATE TABLE "Guardianship" (
- "id" TEXT NOT NULL,
- "guardianUserId" TEXT NOT NULL,
- "dependentProfileId" TEXT NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "revokedAt" TIMESTAMP(3),
-
- CONSTRAINT "Guardianship_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE INDEX "Guardianship_dependentProfileId_idx" ON "Guardianship"("dependentProfileId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "Guardianship_guardianUserId_dependentProfileId_key" ON "Guardianship"("guardianUserId", "dependentProfileId");
-
--- AddForeignKey
-ALTER TABLE "Guardianship" ADD CONSTRAINT "Guardianship_guardianUserId_fkey" FOREIGN KEY ("guardianUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "Guardianship" ADD CONSTRAINT "Guardianship_dependentProfileId_fkey" FOREIGN KEY ("dependentProfileId") REFERENCES "PatientProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-
--- A dependent (no login) must carry a date of birth: the age-of-majority
--- rule that ends guardian access is computed from it. Profiles with a login
--- are unaffected (dateOfBirth stays optional for them).
-ALTER TABLE "PatientProfile" ADD CONSTRAINT "PatientProfile_dependent_has_dob"
- CHECK ("userId" IS NOT NULL OR "dateOfBirth" IS NOT NULL);
+-- Phase 8, issue #42 (docs/adr/0011): replace the unenforced
+-- PatientProfile.guardianUserId string with a real Guardianship relation.
+--
+-- Safety: no code ever wrote guardianUserId, so every row should be NULL.
+-- If any isn't, its meaning is unknown (the old field was explicitly
+-- "don't build on this"), so stop here and let an operator decide rather
+-- than silently dropping or reinterpreting it.
+DO $$
+BEGIN
+ IF EXISTS (SELECT 1 FROM "PatientProfile" WHERE "guardianUserId" IS NOT NULL) THEN
+ RAISE EXCEPTION 'PatientProfile.guardianUserId has non-null values; resolve them before applying guardian_dependent_profiles (see docs/adr/0011).';
+ END IF;
+END $$;
+
+-- AlterTable
+ALTER TABLE "PatientProfile" DROP COLUMN "guardianUserId",
+ALTER COLUMN "userId" DROP NOT NULL;
+
+-- CreateTable
+CREATE TABLE "Guardianship" (
+ "id" TEXT NOT NULL,
+ "guardianUserId" TEXT NOT NULL,
+ "dependentProfileId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "revokedAt" TIMESTAMP(3),
+
+ CONSTRAINT "Guardianship_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "Guardianship_dependentProfileId_idx" ON "Guardianship"("dependentProfileId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Guardianship_guardianUserId_dependentProfileId_key" ON "Guardianship"("guardianUserId", "dependentProfileId");
+
+-- AddForeignKey
+ALTER TABLE "Guardianship" ADD CONSTRAINT "Guardianship_guardianUserId_fkey" FOREIGN KEY ("guardianUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Guardianship" ADD CONSTRAINT "Guardianship_dependentProfileId_fkey" FOREIGN KEY ("dependentProfileId") REFERENCES "PatientProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- A dependent (no login) must carry a date of birth: the age-of-majority
+-- rule that ends guardian access is computed from it. Profiles with a login
+-- are unaffected (dateOfBirth stays optional for them).
+ALTER TABLE "PatientProfile" ADD CONSTRAINT "PatientProfile_dependent_has_dob"
+ CHECK ("userId" IS NOT NULL OR "dateOfBirth" IS NOT NULL);
diff --git a/backend/prisma/migrations/20261022090000_fulfillment_record_typed_order_fks/migration.sql b/backend/prisma/migrations/20261022090000_fulfillment_record_typed_order_fks/migration.sql
index a48dbe4a..e06695f2 100644
--- a/backend/prisma/migrations/20261022090000_fulfillment_record_typed_order_fks/migration.sql
+++ b/backend/prisma/migrations/20261022090000_fulfillment_record_typed_order_fks/migration.sql
@@ -1,36 +1,36 @@
--- Issue #13 (docs/adr/0012): FulfillmentRecord stops being polymorphic.
--- The Phase 0 (orderType, orderId) TEXT pair carried no referential integrity
--- at all, which is what ARCHITECTURE.md Β§6.1.3 flagged. Both are replaced by
--- real FKs with a CHECK requiring exactly one β the same discriminator
--- mechanism PaymentIntent got in issue #85 β and Β§4.7's TODO facility column
--- becomes a real FK to FacilityProfile.
-
--- No code path in backend/src ever wrote this table, and an unvalidated
--- (orderType, orderId) pair cannot be reinterpreted as a typed reference,
--- so abort instead of guessing (same posture as #9's placeholder guard).
-DO $$ BEGIN
- IF EXISTS (SELECT 1 FROM "FulfillmentRecord") THEN
- RAISE EXCEPTION 'Issue #13 migration requires placeholder FulfillmentRecord rows to be reviewed before migration';
- END IF;
-END $$;
-
-ALTER TABLE "FulfillmentRecord" DROP COLUMN "orderType", DROP COLUMN "orderId";
-ALTER TABLE "FulfillmentRecord"
- ADD COLUMN "prescriptionId" TEXT,
- ADD COLUMN "labOrderId" TEXT,
- ADD COLUMN "facilityId" TEXT NOT NULL;
-
--- One fulfillment row per order, ever: Postgres keeps NULLs in a unique
--- index, so each column allows at most one non-null occurrence.
-CREATE UNIQUE INDEX "FulfillmentRecord_prescriptionId_key" ON "FulfillmentRecord"("prescriptionId");
-CREATE UNIQUE INDEX "FulfillmentRecord_labOrderId_key" ON "FulfillmentRecord"("labOrderId");
--- The facility's assigned-orders queue (Β§5.3) filters its own rows by status.
-CREATE INDEX "FulfillmentRecord_facilityId_status_idx" ON "FulfillmentRecord"("facilityId", "status");
-
-ALTER TABLE "FulfillmentRecord" ADD CONSTRAINT "FulfillmentRecord_exactly_one_order_check"
- CHECK (("prescriptionId" IS NOT NULL) <> ("labOrderId" IS NOT NULL));
--- RESTRICT everywhere: neither deleting an order nor deleting a facility
--- account may erase the fulfillment trail (docs/adr/0010's retention rule).
-ALTER TABLE "FulfillmentRecord" ADD CONSTRAINT "FulfillmentRecord_prescriptionId_fkey" FOREIGN KEY ("prescriptionId") REFERENCES "Prescription"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-ALTER TABLE "FulfillmentRecord" ADD CONSTRAINT "FulfillmentRecord_labOrderId_fkey" FOREIGN KEY ("labOrderId") REFERENCES "LabOrder"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-ALTER TABLE "FulfillmentRecord" ADD CONSTRAINT "FulfillmentRecord_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "FacilityProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+-- Issue #13 (docs/adr/0012): FulfillmentRecord stops being polymorphic.
+-- The Phase 0 (orderType, orderId) TEXT pair carried no referential integrity
+-- at all, which is what ARCHITECTURE.md Β§6.1.3 flagged. Both are replaced by
+-- real FKs with a CHECK requiring exactly one β the same discriminator
+-- mechanism PaymentIntent got in issue #85 β and Β§4.7's TODO facility column
+-- becomes a real FK to FacilityProfile.
+
+-- No code path in backend/src ever wrote this table, and an unvalidated
+-- (orderType, orderId) pair cannot be reinterpreted as a typed reference,
+-- so abort instead of guessing (same posture as #9's placeholder guard).
+DO $$ BEGIN
+ IF EXISTS (SELECT 1 FROM "FulfillmentRecord") THEN
+ RAISE EXCEPTION 'Issue #13 migration requires placeholder FulfillmentRecord rows to be reviewed before migration';
+ END IF;
+END $$;
+
+ALTER TABLE "FulfillmentRecord" DROP COLUMN "orderType", DROP COLUMN "orderId";
+ALTER TABLE "FulfillmentRecord"
+ ADD COLUMN "prescriptionId" TEXT,
+ ADD COLUMN "labOrderId" TEXT,
+ ADD COLUMN "facilityId" TEXT NOT NULL;
+
+-- One fulfillment row per order, ever: Postgres keeps NULLs in a unique
+-- index, so each column allows at most one non-null occurrence.
+CREATE UNIQUE INDEX "FulfillmentRecord_prescriptionId_key" ON "FulfillmentRecord"("prescriptionId");
+CREATE UNIQUE INDEX "FulfillmentRecord_labOrderId_key" ON "FulfillmentRecord"("labOrderId");
+-- The facility's assigned-orders queue (Β§5.3) filters its own rows by status.
+CREATE INDEX "FulfillmentRecord_facilityId_status_idx" ON "FulfillmentRecord"("facilityId", "status");
+
+ALTER TABLE "FulfillmentRecord" ADD CONSTRAINT "FulfillmentRecord_exactly_one_order_check"
+ CHECK (("prescriptionId" IS NOT NULL) <> ("labOrderId" IS NOT NULL));
+-- RESTRICT everywhere: neither deleting an order nor deleting a facility
+-- account may erase the fulfillment trail (docs/adr/0010's retention rule).
+ALTER TABLE "FulfillmentRecord" ADD CONSTRAINT "FulfillmentRecord_prescriptionId_fkey" FOREIGN KEY ("prescriptionId") REFERENCES "Prescription"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+ALTER TABLE "FulfillmentRecord" ADD CONSTRAINT "FulfillmentRecord_labOrderId_fkey" FOREIGN KEY ("labOrderId") REFERENCES "LabOrder"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+ALTER TABLE "FulfillmentRecord" ADD CONSTRAINT "FulfillmentRecord_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "FacilityProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/backend/prisma/migrations/20261022090000_referral_expiry/migration.sql b/backend/prisma/migrations/20261022090000_referral_expiry/migration.sql
index 6a49bd19..2f7590cc 100644
--- a/backend/prisma/migrations/20261022090000_referral_expiry/migration.sql
+++ b/backend/prisma/migrations/20261022090000_referral_expiry/migration.sql
@@ -1,16 +1,16 @@
--- Referral expiry job (issue #8). Records when and why a referral expired so
--- a time-based expiry ("ttl") is distinguishable from the no-show cascade
--- ("no_show") and from `declined`/`cancelled`.
-
--- CreateEnum
-CREATE TYPE "ReferralExpiryReason" AS ENUM ('ttl', 'no_show');
-
--- AlterTable
-ALTER TABLE "Referral" ADD COLUMN "expiredAt" TIMESTAMP(3),
-ADD COLUMN "expiredReason" "ReferralExpiryReason";
-
--- CreateIndex
-CREATE INDEX "Referral_status_updatedAt_idx" ON "Referral"("status", "updatedAt");
-
--- Backfill: until now the only path to `expired` was the no-show cascade.
-UPDATE "Referral" SET "expiredAt" = "updatedAt", "expiredReason" = 'no_show' WHERE "status" = 'expired';
+-- Referral expiry job (issue #8). Records when and why a referral expired so
+-- a time-based expiry ("ttl") is distinguishable from the no-show cascade
+-- ("no_show") and from `declined`/`cancelled`.
+
+-- CreateEnum
+CREATE TYPE "ReferralExpiryReason" AS ENUM ('ttl', 'no_show');
+
+-- AlterTable
+ALTER TABLE "Referral" ADD COLUMN "expiredAt" TIMESTAMP(3),
+ADD COLUMN "expiredReason" "ReferralExpiryReason";
+
+-- CreateIndex
+CREATE INDEX "Referral_status_updatedAt_idx" ON "Referral"("status", "updatedAt");
+
+-- Backfill: until now the only path to `expired` was the no-show cascade.
+UPDATE "Referral" SET "expiredAt" = "updatedAt", "expiredReason" = 'no_show' WHERE "status" = 'expired';
diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma
index 2e4f8999..c4640646 100644
--- a/backend/prisma/schema.prisma
+++ b/backend/prisma/schema.prisma
@@ -1,1189 +1,1189 @@
-// schema.prisma β DocForum canonical data model
-// SOURCE OF TRUTH: if this file and ARCHITECTURE.md Β§4 disagree, fix ARCHITECTURE.md
-// to match this file (or vice versa) in the same PR β see AGENTS.md.
-//
-// STATUS: Phase 1 models (User/PatientProfile/DoctorProfile/FacilityProfile/
-// Specialty/DoctorSpecialty/FacilityCapability/AvailabilitySlot/Appointment)
-// are fleshed out and migrated. Everything below the Phase 1 marker is still
-// the Phase 0 structural placeholder β see ROADMAP.md.
-
-generator client {
- provider = "prisma-client-js"
-}
-
-// Phase 8, issue #47: Prisma 7 no longer accepts `url` in the datasource
-// block β the connection is configured in prisma.config.ts (for the CLI:
-// migrate/generate/diff) and passed to PrismaClient as a driver adapter at
-// runtime (see src/db/prisma-client.ts).
-datasource db {
- provider = "postgresql"
-}
-
-enum Role {
- patient
- doctor
- facility
- admin
-}
-
-enum VerificationStatus {
- pending
- verified
- rejected
-}
-
-enum FacilityType {
- lab
- pharmacy
-}
-
-enum CapabilityType {
- lab_test
- medication_dispense
-}
-
-enum SlotStatus {
- open
- held
- booked
-}
-
-enum AppointmentStatus {
- scheduled
- completed
- cancelled
- no_show
-}
-
-enum AppointmentOrigin {
- direct_booking
- referral
-}
-
-enum ConsultationOutcome {
- resolved
- referred
- follow_up_needed
-}
-
-enum ReferralStatus {
- pending
- accepted
- declined
- completed
- expired
- // Admin-initiated termination (issue #68) β distinct from `expired`
- // (the cascade gave up after repeated no-shows) and `declined` (the
- // patient said no) so audit/metrics can tell the three exits apart.
- cancelled
-}
-
-// Why a referral reached the terminal `expired` state. Kept separate from
-// `status` so `expired` stays one state (and stays distinct from `declined`
-// and `cancelled`) while audit/metrics/notifications can still tell the two
-// ways in apart: `ttl` = nobody accepted/declined it in time (issue #8),
-// `no_show` = the cascade gave up after repeated no-shows (issue #67).
-enum ReferralExpiryReason {
- ttl
- no_show
-}
-
-enum OrderStatus {
- issued
- fulfilled
- expired
- superseded
-}
-
-enum OrderType {
- pharmacy
- lab
-}
-
-enum DisputeStatus {
- pending
- resolved
-}
-
-enum FulfillmentStatus {
- pending
- in_progress
- fulfilled
- rejected
-}
-
-enum Urgency {
- routine
- urgent
-}
-
-// =========================================================================
-// Phase 1 models β identity, DB, safe booking foundation. See ROADMAP.md.
-// =========================================================================
-
-model User {
- id String @id @default(uuid())
- email String @unique
- phone String?
- passwordHash String
- role Role
- // Phase 8 (issue #34): null = never verified. Self-serve signup leaves
- // this null; existing rows were backfilled to `createdAt` in the
- // add_email_verification migration. Distinct from
- // DoctorProfile.verificationStatus, which is the separate, manual
- // admin verification of a doctor's credentials β do not conflate.
- emailVerifiedAt DateTime?
- // Phase 9, issue #133: server-generated text preference. Values are
- // normalized to a supported language tag at signup; unsupported tags
- // fall back to English.
- locale String @default("en")
- // Phase 8 (issue #39): null = active. Deactivating never deletes anything
- // β no clinical or payment record is keyed to this flag, and none of the
- // deactivation paths remove rows. Login, refresh and access tokens are all
- // refused while this is set.
- deactivatedAt DateTime?
- // Phase 8 (issue #39): bumped when an account is deactivated/reactivated,
- // so access tokens minted before that moment stop working immediately
- // instead of living out their 15-minute TTL. There is still no general
- // refresh-token revocation list (issue #18) β this is the narrow "revoke
- // everything for one account" case that deactivation needs.
- sessionsInvalidatedAt DateTime?
- // Per-user refresh-token version. Embedded in every access/refresh token
- // as `tv` and compared against this column at refresh time. Bumping it
- // (password reset, Phase 8 #33) invalidates every outstanding refresh
- // token for the user, since none of them carry the new value β the
- // interim kill switch issue #33 asked for while #18 doesn't exist yet.
- tokenVersion Int @default(0)
- // Phase 8 (issue #41, docs/adr/0010): set once, irreversibly, when the
- // account is deleted. The row is anonymised in place (email tombstoned,
- // phone cleared, password unusable) rather than deleted, because clinical
- // and payment records still reference its patient profile. Refresh is
- // refused once set. Distinct from admin deactivation (#39), which is
- // reversible.
- deletedAt DateTime?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- patientProfile PatientProfile?
- doctorProfile DoctorProfile?
- facilityProfile FacilityProfile?
- refreshSessions RefreshSession[]
-
- emailVerificationTokens EmailVerificationToken[]
- passwordResetTokens PasswordResetToken[]
- adminInvitesSent AdminInvite[]
- // Phase 8 (issue #45): PHI read-access log rows this user wrote (as viewer).
- // Opposite of AccessLog.accessedByUser.
- accessLogs AccessLog[]
- // Phase 8 (issue #42, docs/adr/0011): dependents this user is guardian of.
- guardianships Guardianship[]
- orderDisputeResolutions OrderDisputeResolution[]
- sep10Challenges Sep10Challenge[]
-
- @@index([role])
-}
-
-// Phase 8 (issue #34): hashed, single-use, expiring email-verification
-// tokens. Only the SHA-256 hash is persisted β a database leak must not
-// hand out working verification links. One row per issued token so a
-// resend can be rate-limited against the most recent `createdAt`.
-model EmailVerificationToken {
- id String @id @default(uuid())
- userId String
- tokenHash String @unique
- expiresAt DateTime
- consumedAt DateTime?
- createdAt DateTime @default(now())
-
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
-
- @@index([userId])
-}
-
-// Password reset tokens (Phase 8 #33). Only the SHA-256 hash of the
-// token is ever stored β the raw token exists exactly once, in the
-// notification that carries the reset link, and is never logged or
-// persisted. Single-use (`usedAt`) and expiring (`expiresAt`).
-model PasswordResetToken {
- id String @id @default(uuid())
- userId String
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
- // SHA-256 hex of the raw token. Unique so lookup-by-hash is a single
- // indexed read, and a duplicate hash (astronomically unlikely) is a
- // constraint violation rather than a silent overwrite.
- tokenHash String @unique
- expiresAt DateTime
- // Non-null once used or superseded by a newer request β either way the
- // token is dead. Requesting a second reset marks the first one used.
- usedAt DateTime?
- createdAt DateTime @default(now())
-
- @@index([userId])
-}
-
-model AdminInvite {
- id String @id @default(uuid())
- email String
- tokenHash String @unique
- expiresAt DateTime
- consumedAt DateTime?
- invitedByUserId String
- createdAt DateTime @default(now())
-
- invitedBy User @relation(fields: [invitedByUserId], references: [id], onDelete: Cascade)
-
- @@index([email])
-}
-
-// Phase 8 (issue #35): login-throttle state. Deliberately in Postgres and
-// not in process memory β the backend runs as a single Render instance
-// today but must not assume that forever, and a restart must not hand an
-// attacker a fresh budget. One row per (scope, key) bucket, where scope is
-// `email` (normalised address) or `ip` (client IP, after the proxy chain is
-// resolved β see TRUST_PROXY in src/config/index.ts).
-//
-// Counters are only ever moved by a single conditional upsert (see
-// auth/repositories `recordFailedLoginAttempt`), never read-then-write: two
-// concurrent bad logins for the same account must both be counted.
-model LoginAttempt {
- id String @id @default(uuid())
- scope String // 'email' | 'ip'
- key String // normalised email, or client IP
- failedCount Int @default(0)
- // Start of the current sliding window; a failure outside the window
- // restarts the count at 1 instead of accumulating forever.
- windowStart DateTime @default(now())
- lastFailedAt DateTime @default(now())
- // Set once this bucket has hit the lock threshold; cleared by a
- // successful login, an admin clear, or the start of a new window.
- lockUntil DateTime?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- @@unique([scope, key])
- @@index([lastFailedAt])
-}
-
-// One row per refresh-token *family*, i.e. per login. This is the
-// revocation store session management needs (Phase 8 #38) and the shape
-// issue #18's refresh-token revocation wanted: the refresh JWT carries
-// `sid`, and a refresh is only honoured while the row exists with
-// `revokedAt` still null.
-//
-// Collecting a coarse IP and user agent here is deliberate β it is what
-// makes "sign out that other device" usable. Nothing clinical or
-// identifying beyond what the request itself already carried.
-model RefreshSession {
- id String @id @default(uuid())
- userId String
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
-
- createdAt DateTime @default(now())
- // Updated on refresh only β never on every access-token-authenticated
- // request, so this doesn't become a write hot-spot on the User row.
- lastUsedAt DateTime @default(now())
- revokedAt DateTime?
-
- userAgent String?
- ip String?
-
- @@index([userId])
- @@index([userId, revokedAt])
-}
-
-model PatientProfile {
- id String @id @default(uuid())
- // Null for a dependent (Phase 8, issue #42, docs/adr/0011): a
- // guardian-owned profile with no login of its own. Still unique, so a
- // login owns at most one profile. A DB CHECK (see the
- // guardian_dependent_profiles migration) requires dateOfBirth whenever
- // userId is null β the age-of-majority rule depends on it.
- userId String? @unique
- fullName String
- dateOfBirth DateTime?
- // Phase 8 (issue #41, docs/adr/0010): set when the owner deleted their
- // account. fullName/dateOfBirth are anonymised at the same moment; the row
- // itself stays so appointments and payments keep their foreign key.
- // getMyPatientProfile refuses an anonymised profile.
- anonymizedAt DateTime?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- // RESTRICT, not CASCADE (issue #41): a patient's User row is never
- // hard-deleted while this profile exists β deletion is anonymisation
- // (ADR 0010), and an accidental user delete must fail loudly rather than
- // take the clinical identity anchor with it. Optional since issue #42: a
- // dependent profile has no login.
- user User? @relation(fields: [userId], references: [id], onDelete: Restrict)
- appointments Appointment[]
- paymentIntents PaymentIntent[]
- prescriptions Prescription[]
- labOrders LabOrder[]
- dataExports PatientExport[]
- // Phase 3: the referrals this patient is the subject of (the opposite
- // side of Referral.patientProfileId).
- referrals Referral[]
- // ADR 0005 step 1 (issue #80): the patient's verified Stellar payer
- // address, when one has been linked. Optional because linking is a
- // separate flow that nothing creates rows for yet.
- walletLink PatientWalletLink?
-
- // Phase 8 (issue #45): who viewed this patient's records.
- accessLogs AccessLog[]
-
- // Phase 8 (issue #42, docs/adr/0011): guardians of this (dependent) profile.
- guardians Guardianship[]
- orders Order[]
- orderDisputes OrderDispute[]
-}
-
-// Phase 8 (issue #40): right-of-access export jobs. One row per request;
-// the generated bundle itself lives in PatientExportChunk (below), not in
-// this row. Lifecycle: pending -> processing -> ready -> expired, or
-// -> failed. A job only moves forward through conditional UPDATEs
-// (`WHERE status = ...`), the same shape as the booking guarantee, so two
-// workers can never both generate one export.
-enum PatientExportStatus {
- pending
- processing
- ready
- failed
- expired
-}
-
-model PatientExport {
- id String @id @default(uuid())
- // The owner. Every read is scoped by this, never by the export id alone.
- patientProfileId String
- status PatientExportStatus @default(pending)
- // Bundle format version actually written (set when generation starts).
- schemaVersion Int?
- // Fencing token: incremented on every claim. Chunks are keyed by it, and
- // the final `ready` transition is conditional on it, so a worker whose
- // claim went stale can never publish a half-written or superseded bundle.
- attempt Int @default(0)
- startedAt DateTime?
- generatedAt DateTime?
- // Set when the job becomes `ready`; downloads are refused at or after it
- // even if the cleanup job hasn't run yet.
- expiresAt DateTime?
- sizeBytes Int?
- // How many chunks the published attempt wrote. A download that finds
- // fewer (cleanup ran mid-stream) aborts instead of handing out a
- // silently truncated JSON document.
- chunkCount Int?
- // Machine-readable failure only (e.g. SECTION_FAILED + section name) β
- // never an exception message, which could echo patient data.
- failureCode String?
- failedSection String?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id], onDelete: Cascade)
- chunks PatientExportChunk[]
-
- @@index([patientProfileId, createdAt])
- @@index([status, expiresAt])
-}
-
-// The export bundle, stored as ordered text chunks of one JSON document
-// (issue #40). Written incrementally while sections are paged, read back
-// one chunk at a time on download, so neither side ever holds a whole
-// patient history in memory. Kept in Postgres rather than object storage
-// because no object-storage provider is integrated yet (ARCHITECTURE.md Β§2
-// and Β§6.3.4) β only patients/repositories reads or writes it, so moving to
-// S3 later replaces that storage seam, not the export design.
-model PatientExportChunk {
- exportId String
- attempt Int
- seq Int
- data String
-
- export PatientExport @relation(fields: [exportId], references: [id], onDelete: Cascade)
-
- @@id([exportId, attempt, seq])
-}
-
-// Phase 8 (issue #42, docs/adr/0011): replaces the unenforced
-// PatientProfile.guardianUserId string. A guardian (a patient User) owns a
-// dependent PatientProfile they created; there is deliberately no way to
-// link an existing profile. Active = revokedAt is null AND the dependent is
-// under AGE_OF_MAJORITY_YEARS (checked at request time from the DOB β no
-// job flips anything). Never deleted: ending a guardianship sets revokedAt,
-// so who had access, and when, stays on record.
-model Guardianship {
- id String @id @default(uuid())
- guardianUserId String
- dependentProfileId String
- createdAt DateTime @default(now())
- revokedAt DateTime?
-
- guardian User @relation(fields: [guardianUserId], references: [id], onDelete: Restrict)
- dependentProfile PatientProfile @relation(fields: [dependentProfileId], references: [id], onDelete: Restrict)
-
- // One row per (guardian, dependent) pair β no duplicate or ambiguous
- // relationships. More than one guardian per dependent is allowed by the
- // model (two parents), though v1's API only creates one.
- @@unique([guardianUserId, dependentProfileId])
- @@index([dependentProfileId])
-}
-
-model DoctorProfile {
- id String @id @default(uuid())
- userId String @unique
- fullName String
- verificationStatus VerificationStatus @default(pending)
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
- specialties DoctorSpecialty[]
- availabilitySlots AvailabilitySlot[]
- appointments Appointment[]
- prescriptions Prescription[]
- labOrders LabOrder[]
- // Referral sides a doctor can sit on: `referringReferrals` are ones
- // they created; `targetedReferrals` are ones aimed at them (whether
- // they ultimately see the patient or not).
- referringReferrals Referral[] @relation("ReferralReferringDoctor")
- targetedReferrals Referral[] @relation("ReferralTargetDoctor")
-
- @@index([verificationStatus])
-}
-
-model Specialty {
- id String @id @default(uuid())
- name String @unique
- slug String @unique
-
- doctors DoctorSpecialty[]
-}
-
-model DoctorSpecialty {
- id String @id @default(uuid())
- doctorProfileId String
- specialtyId String
-
- doctorProfile DoctorProfile @relation(fields: [doctorProfileId], references: [id], onDelete: Cascade)
- specialty Specialty @relation(fields: [specialtyId], references: [id], onDelete: Cascade)
-
- @@unique([doctorProfileId, specialtyId])
-}
-
-model FacilityProfile {
- id String @id @default(uuid())
- userId String @unique
- name String
- type FacilityType
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
- capabilities FacilityCapability[]
- prescriptions Prescription[]
- labOrders LabOrder[]
- labResults LabResult[]
- paymentIntents PaymentIntent[]
- fulfillments FulfillmentRecord[]
- walletLink WalletLink?
- prices FacilityPrice[]
- orders Order[]
- fulfillmentProofs FulfillmentProof[]
-}
-
-model FacilityCapability {
- id String @id @default(uuid())
- facilityProfileId String
- capabilityType CapabilityType
- // e.g. a lab test category, or a medication class β free text on purpose,
- // no controlled vocabulary decided yet (out of Phase 1 scope).
- detail String?
-
- facilityProfile FacilityProfile @relation(fields: [facilityProfileId], references: [id], onDelete: Cascade)
-
- @@unique([facilityProfileId, capabilityType, detail])
-}
-
-/// Versioned facility price list (issue #127). Rows are append-only: changing
-/// a price inserts a new row with a later `effectiveFrom`, so history is kept
-/// and an intent priced earlier keeps resolving against what was in force at
-/// its selection time. Amounts are stroops (BigInt) like PaymentIntent.
-///
-/// `catalogItemCode` is deliberately an open vocabulary (a lab test or
-/// medication code the facility publishes), matching FacilityCapability.detail
-/// β no controlled vocabulary has been decided yet.
-model FacilityPrice {
- id String @id @default(uuid())
- facilityId String
- facility FacilityProfile @relation(fields: [facilityId], references: [id], onDelete: Cascade)
- catalogItemCode String
- // Stellar asset the price is denominated in. v1 quotes native XLM
- // ('native'); the column exists so a price list can add assets without a
- // schema change when multi-asset payments land.
- asset String
- amount BigInt
- effectiveFrom DateTime
- createdAt DateTime @default(now())
-
- // Two versions of the same item/asset cannot share an effective instant.
- @@unique([facilityId, catalogItemCode, asset, effectiveFrom])
- @@index([facilityId, catalogItemCode, asset, effectiveFrom])
- @@index([catalogItemCode, asset, effectiveFrom])
-}
-
-model AvailabilitySlot {
- id String @id @default(uuid())
- doctorProfileId String
- status SlotStatus @default(open)
- startTime DateTime
- endTime DateTime
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- doctorProfile DoctorProfile @relation(fields: [doctorProfileId], references: [id], onDelete: Cascade)
- appointment Appointment?
-
- // Prevents two slot ROWS for the same doctor/time range (duplicate
- // generation). This is NOT what prevents double-BOOKING of one slot by
- // two patients β that's the conditional `UPDATE ... WHERE status =
- // 'open'` in availability/services, inside a transaction. See
- // ARCHITECTURE.md Β§5.1 and the integration test in
- // backend/tests/integration/booking.test.ts.
- @@unique([doctorProfileId, startTime, endTime])
- @@index([doctorProfileId, status])
-}
-
-model Appointment {
- id String @id @default(uuid())
- patientProfileId String
- doctorProfileId String
- // Nullable because a CANCELLED appointment releases its slot back to
- // `open` (referral cascade / patient cancel) β the appointment keeps its
- // history row but must stop holding the unique claim on the slot, or
- // re-booking the freed slot would violate the unique constraint.
- // The booking guarantee itself is unchanged: claimOpenSlot's
- // conditional UPDATE on SlotStatus is what prevents double-booking.
- availabilitySlotId String? @unique
- status AppointmentStatus @default(scheduled)
- origin AppointmentOrigin @default(direct_booking)
- // Set when this appointment was produced by accepting a Referral (issue
- // #7). The cascade rules (#67 / PRD Β§8.2.3) react to status transitions
- // on exactly these appointments. NOT unique: one referral can produce
- // several appointments over its life (re-booking after a cancellation).
- // Referral.resultingAppointmentId is the unique "current" pointer.
- referralId String?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- // Explicit RESTRICT (was the Prisma default β same SQL): an appointment is
- // a clinical record and must outlive any account deletion (ADR 0010).
- patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id], onDelete: Restrict)
- doctorProfile DoctorProfile @relation(fields: [doctorProfileId], references: [id])
- availabilitySlot AvailabilitySlot? @relation(fields: [availabilitySlotId], references: [id])
- referral Referral? @relation("ReferralOriginatedAppointments", fields: [referralId], references: [id])
- // Non-owning side of Referral.resultingAppointmentId (the FK lives on
- // Referral) β which referral currently points at this appointment as
- // the one it produced.
- referralResultOf Referral? @relation("ReferralResultingAppointment")
- // Phase 2 (issue #4): the appointment's consultation. 1:1 β the uniqueness
- // is Consultation.appointmentId UNIQUE (docs/adr/0009), so this side is a
- // plain back-relation.
- consultation Consultation?
-
- // TODO Phase 2: intakeId (FK -> IntakeForm). PRD FR-5 requires intake
- // before confirmation, not enforced yet β booking works without it in
- // Phase 1 so the concurrency-safety work isn't blocked on IntakeForm
- // existing. Flagged, not silently dropped.
-}
-
-// =========================================================================
-model AccessLog {
- id String @id @default(uuid())
- // Who viewed. SetNull on user delete keeps the log row (timestamp +
- // entity remain) while removing the PII pointer.
- accessedBy String?
- // Which patient's records were viewed (the record owner, not the viewer).
- patientProfileId String?
- // Entity that was read: 'IntakeForm' | 'Consultation' | 'Prescription' |
- // 'LabOrder' | 'LabResult' | 'Referral' | 'PaymentIntent' | 'PatientProfile'
- entityType String
- entityId String
- // The HTTP route that returned the payload (for correlating with access logs).
- route String?
- // Free-text detail β no PHI here, just context like "GET /appointments/mine".
- detail String?
- accessedAt DateTime @default(now())
-
- accessedByUser User? @relation(fields: [accessedBy], references: [id], onDelete: SetNull)
- patientProfile PatientProfile? @relation(fields: [patientProfileId], references: [id], onDelete: Cascade)
-
- @@index([accessedAt])
- @@index([accessedBy])
- @@index([patientProfileId, accessedAt])
- @@index([entityType, entityId])
-}
-
-// =========================================================================
-// Orders, Fulfillment Proofs, Disputes & Resolutions
-// =========================================================================
-
-model Order {
- id String @id @default(uuid())
- orderType OrderType
- patientProfileId String
- facilityProfileId String?
- prescribingDoctorId String?
- details String?
- status OrderStatus @default(issued)
-
- fulfilledAt DateTime?
-
- confirmed Boolean @default(false)
- confirmationSource String? // 'patient' | 'timeout'
- confirmedAt DateTime?
-
- disputed Boolean @default(false)
- disputedAt DateTime?
-
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id])
- facilityProfile FacilityProfile? @relation(fields: [facilityProfileId], references: [id])
-
- fulfillmentProofs FulfillmentProof[]
- disputes OrderDispute[]
-
- @@index([patientProfileId, status])
- @@index([facilityProfileId, status])
- @@index([status, confirmed, disputed])
-}
-
-model FulfillmentProof {
- id String @id @default(uuid())
- orderId String
- facilityProfileId String
- referenceNumber String
- attachmentUrl String?
- notes String?
- createdAt DateTime @default(now())
-
- order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
- facilityProfile FacilityProfile @relation(fields: [facilityProfileId], references: [id])
-
- @@index([orderId])
- @@index([facilityProfileId])
-}
-
-model OrderDispute {
- id String @id @default(uuid())
- orderId String
- patientProfileId String
- reason String
- status DisputeStatus @default(pending)
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
- patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id])
- resolutions OrderDisputeResolution[]
-
- @@index([orderId])
- @@index([patientProfileId])
- @@index([status])
-}
-
-model OrderDisputeResolution {
- id String @id @default(uuid())
- disputeId String
- adminUserId String
- resolutionNotes String
- outcome String // e.g. 'upheld' | 'rejected' | 'refunded'
- createdAt DateTime @default(now())
-
- dispute OrderDispute @relation(fields: [disputeId], references: [id], onDelete: Cascade)
- adminUser User @relation(fields: [adminUserId], references: [id])
-
- @@index([disputeId])
- @@index([adminUserId])
-}
-
-// Below this line: Phase 0 structural placeholders, not yet implemented.
-// See ROADMAP.md for phase order.
-// =========================================================================
-
-model IntakeForm {
- id String @id @default(uuid())
- createdAt DateTime @default(now())
- // TODO: patientProfileId, symptoms (Json), freeTextNotes
-}
-
-// Phase 3 (issue #5 minimal + lineage for #65): a Consultation is the
-// record of a doctor having seen the patient at a specific appointment.
-// `appointmentId` is the lineage seam β Referral.parentReferralId is
-// derived through it (consultation β appointment β referral), which is
-// what loop detection walks. Full consultation-note workflow (append-only
-// addenda, sign-off) stays issue #4/#64's scope.
-model Consultation {
- id String @id @default(uuid())
- // unique = "a consultation is closed once per appointment", enforced by the
- // database rather than by a read-then-write check (docs/adr/0009).
- appointmentId String @unique
- // Nullable so a consultation can exist before sign-off (Phase 3's referral
- // slice); the close routes require it, so a closed record always has one.
- outcome ConsultationOutcome?
- // Clinical free text: stored as an AES-256-GCM envelope by
- // consultations/services (issue #46 β ARCHITECTURE.md Β§4.3), never plaintext.
- notes String?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- // RESTRICT (Prisma's default for a required relation) is what the existing
- // migration created: a clinical record must not disappear because an
- // appointment row was deleted.
- appointment Appointment @relation(fields: [appointmentId], references: [id])
- prescriptions Prescription[]
- labOrders LabOrder[]
- // Phase 3: the referrals that grew out of this consultation (the
- // opposite side of Referral.sourceConsultationId).
- referrals Referral[]
-}
-
-// Phase 3 (issues #5/#65/#66/#67/#68). Snapshot rule (FR-7/FR-8, hard
-// rule 3): `snapshotNotes` is a copy taken at creation, never a live
-// reference β reassign/cancel/cascade never touch it.
-model Referral {
- id String @id @default(uuid())
- status ReferralStatus @default(pending)
- patientProfileId String
- // The consultation this referral grew out of. Required: every referral
- // is created from a consultation (PRD FR-7), which is also what gives
- // loop detection its lineage.
- sourceConsultationId String
- referringDoctorId String
- // Always a concrete doctor: a "specialty, no specific doctor" referral
- // is routed to one at creation (least-loaded eligible doctor β issue
- // #6, docs/adr/0008). The column stays NOT NULL on purpose, so an
- // unassigned referral cannot exist; when nobody is eligible the
- // creation fails instead of writing a row.
- targetDoctorId String
- // Optional denormalised label of the target specialty, used by the
- // loop guard and admin views. Not a substitute for issue #23's real
- // per-doctor specialty management.
- targetSpecialtyId String?
- snapshotNotes String
- // Issue #65: the referral whose appointment produced the consultation
- // this referral came from β the chain loop detection walks (bounded).
- parentReferralId String?
- // Issue #67 bookkeeping: how many times a referral-origin appointment
- // has no-showed, and the "the referring doctor must know" flag set
- // when repeated no-shows expire the referral. Both live on the
- // referral (not a Notification row) because Phase 6's dispatch layer
- // doesn't exist yet β the flag is the seam it will read.
- noShowCount Int @default(0)
- referringDoctorNotifiedAt DateTime?
- // Issue #65: set (with a reason naming the matching hop) when the
- // target doctor/specialty was already in the ancestor chain β the
- // referral is still created (a human decides), but surfaces in the
- // admin queue.
- loopFlaggedAt DateTime?
- loopFlagReason String?
- // Issue #68: admin-initiated cancellation carries its reason; the
- // audit trail proper is the AuditEvent row written alongside.
- cancelledReason String?
- // The appointment this referral currently produced (null while
- // pending/declined, or after a cancelled/no-showed appointment returns
- // it to pending). Kept pointing at the last no-show when expired β
- // that appointment is why it expired.
- resultingAppointmentId String? @unique
- // Issue #8: set together with status `expired` β when, and why.
- expiredAt DateTime?
- expiredReason ReferralExpiryReason?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- sourceConsultation Consultation @relation(fields: [sourceConsultationId], references: [id])
- patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id])
- referringDoctor DoctorProfile @relation("ReferralReferringDoctor", fields: [referringDoctorId], references: [id])
- targetDoctor DoctorProfile @relation("ReferralTargetDoctor", fields: [targetDoctorId], references: [id])
- parentReferral Referral? @relation("ReferralLineage", fields: [parentReferralId], references: [id])
- childReferrals Referral[] @relation("ReferralLineage")
- resultingAppointment Appointment? @relation("ReferralResultingAppointment", fields: [resultingAppointmentId], references: [id])
- // Every appointment this referral ever produced (including cancelled/
- // no-showed ones) β history survives the resultingAppointmentId link
- // being cleared when the referral returns to pending.
- appointments Appointment[] @relation("ReferralOriginatedAppointments")
-
- @@index([patientProfileId])
- @@index([referringDoctorId])
- @@index([targetDoctorId])
- @@index([status])
- @@index([status, updatedAt])
- @@index([loopFlaggedAt])
-}
-
-// Issue #68: who did what to a referral, when, and why. Deliberately NOT a
-// second audit table: the append-only `AuditEvent` in this schema (Phase 8,
-// issue #43) is the one audit trail, so referral admin actions are recorded
-// as `entityType: 'Referral'` rows through the audit module
-// (`auditService.record`) β same table, same triggers, same writer.
-
-model Prescription {
- id String @id @default(uuid())
- consultationId String
- patientProfileId String
- prescribingDoctorId String
- facilityProfileId String
- amountStroops BigInt
- status OrderStatus @default(issued)
- supersedesPrescriptionId String?
- createdAt DateTime @default(now())
-
- consultation Consultation @relation(fields: [consultationId], references: [id])
- patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id])
- prescribingDoctor DoctorProfile @relation(fields: [prescribingDoctorId], references: [id])
- facilityProfile FacilityProfile @relation(fields: [facilityProfileId], references: [id])
- supersedes Prescription? @relation("PrescriptionCorrections", fields: [supersedesPrescriptionId], references: [id])
- correctedBy Prescription? @relation("PrescriptionCorrections")
- items PrescriptionItem[]
- paymentIntent PaymentIntent?
- fulfillment FulfillmentRecord?
-
- @@unique([supersedesPrescriptionId])
- @@index([patientProfileId, createdAt])
- @@index([consultationId])
-}
-
-model PrescriptionItem {
- id String @id @default(uuid())
- prescriptionId String
- drugName String
- dosage String
- frequency String
- duration String
-
- prescription Prescription @relation(fields: [prescriptionId], references: [id])
-
- @@index([prescriptionId])
-}
-
-model LabOrder {
- id String @id @default(uuid())
- consultationId String
- patientProfileId String
- orderingDoctorId String
- facilityProfileId String
- amountStroops BigInt
- testType String
- urgency Urgency @default(routine)
- notes String?
- status OrderStatus @default(issued)
- supersedesLabOrderId String?
- createdAt DateTime @default(now())
-
- consultation Consultation @relation(fields: [consultationId], references: [id])
- patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id])
- orderingDoctor DoctorProfile @relation(fields: [orderingDoctorId], references: [id])
- facilityProfile FacilityProfile @relation(fields: [facilityProfileId], references: [id])
- supersedes LabOrder? @relation("LabOrderCorrections", fields: [supersedesLabOrderId], references: [id])
- correctedBy LabOrder? @relation("LabOrderCorrections")
- result LabResult?
- paymentIntent PaymentIntent?
- fulfillment FulfillmentRecord?
-
- @@unique([supersedesLabOrderId])
- @@index([patientProfileId, createdAt])
- @@index([consultationId])
-}
-
-model LabResult {
- id String @id @default(uuid())
- labOrderId String @unique
- fulfillingFacilityId String
- resultFileUrl String?
- summary String?
- createdAt DateTime @default(now())
-
- labOrder LabOrder @relation(fields: [labOrderId], references: [id])
- fulfillingFacility FacilityProfile @relation(fields: [fulfillingFacilityId], references: [id])
-}
-
-/// Facility-side fulfillment state for one issued order. Kept separate from
-/// the order so a facility never needs write access to the clinical record
-/// (ARCHITECTURE.md Β§4.7, hard rule 5).
-///
-/// Issue #13 / docs/adr/0012: the Phase 0 sketch here was polymorphic
-/// (`orderType` + `orderId` TEXT). Both are replaced by real FKs β exactly one
-/// of them is set, required by a PostgreSQL CHECK, and the UNIQUE on each
-/// means an order can have at most one fulfillment row. Same discriminator
-/// mechanism `PaymentIntent` adopted in issue #85.
-///
-/// `facilityId` is copied from the order's own facility at creation (the
-/// creating service in Phase 5 issue #14 owns that rule) so the facility's
-/// queue is indexable on this table and ownership lives on the row the
-/// facility writes.
-model FulfillmentRecord {
- id String @id @default(uuid())
- prescriptionId String? @unique
- labOrderId String? @unique
- facilityId String
- status FulfillmentStatus @default(pending)
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- prescription Prescription? @relation(fields: [prescriptionId], references: [id], onDelete: Restrict)
- labOrder LabOrder? @relation(fields: [labOrderId], references: [id], onDelete: Restrict)
- facility FacilityProfile @relation(fields: [facilityId], references: [id], onDelete: Restrict)
-
- @@index([facilityId, status])
-}
-
-model Notification {
- id String @id @default(uuid())
- type String
- payload Json
- readAt DateTime?
- createdAt DateTime @default(now())
- // TODO: userId
-}
-
-// --- Stellar payments (docforum-org split) -------------------------------
-// These two models hold relational metadata ONLY β amounts, status, tx
-// hashes, public keys. No clinical/PHI field ever belongs here. The actual
-// escrow contract logic lives in the sibling docforum-escrow repo and is
-// called via the @docforum/escrow-sdk package from
-// backend/src/modules/payments/services β never modeled or reimplemented
-// in this schema. See docs/adr/0002-stellar-escrow-for-fulfillment-payout.md
-// and docs/adr/0004-custodial-payments-v1.md.
-//
-// One of prescriptionId/labOrderId is required by a PostgreSQL check
-// constraint. Amount and recipient are snapshotted from the issued order.
-
-enum PaymentIntentStatus {
- created
- // ADR 0005 steps 2-4: a prepared (unsigned) create_escrow transaction
- // exists, then is being submitted with the patient's signature. The
- // submit half is not built yet (issue #80 only covers prepare), so
- // `submitting` is reserved by the state machine but not written today.
- awaiting_signature
- submitting
- escrowed
- released
- refunded
- failed
-}
-
-model PaymentIntent {
- id String @id @default(uuid())
- prescriptionId String? @unique
- labOrderId String? @unique
- status PaymentIntentStatus @default(created)
-
- prescription Prescription? @relation(fields: [prescriptionId], references: [id])
- labOrder LabOrder? @relation(fields: [labOrderId], references: [id])
-
- patientProfileId String
- // Explicit RESTRICT (was the Prisma default β same SQL): a payment record
- // must outlive any account deletion (ADR 0010).
- patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id], onDelete: Restrict)
- facilityId String
- facility FacilityProfile @relation(fields: [facilityId], references: [id])
-
- // Amount in the smallest unit of `assetCode` (stroops for XLM, 7-decimal
- // units for USDC) β matches @docforum/escrow-sdk's `amount: bigint`
- // exactly. Renamed from `amountStroops` by issue #79; the migration is a
- // plain column rename, so existing rows keep their value. This module
- // never scales or converts amounts, so it never assumes an asset's
- // decimals (the allowlist carries them for clients only).
- amount BigInt
- // The asset this intent is denominated in β `XLM` or `USDC` in the
- // default testnet allowlist. Assets are identified on-chain by the SAC
- // contract id, not by the code, so a new intent must name an allowlisted
- // asset (see config.payments.assets and issue #79).
- assetCode String
- // The Stellar Asset Contract address the escrow pulls from. Nullable only
- // for rows created before issue #79: those were funded against
- // `ESCROW_TOKEN_ID` (native XLM), which the service falls back to when it
- // sees a null here. New rows always store it.
- tokenContractId String?
-
- // ADR 0005 step 2 (issue #80). `preparedTxHash` is the hash of the
- // unsigned create_escrow transaction the patient must sign;
- // `preparedXdr` lets a retry return exactly the same transaction instead
- // of building a second one. Both are cleared when the preparation
- // expires back to `created`.
- preparedTxHash String?
- preparedXdr String?
- preparedExpiryLedger Int?
- preparedExpiresAt DateTime?
- // ADR 0005: escrow refund deadline = order expiry + grace. Orders don't
- // exist as real records yet (Phase 4), so the service uses the intent's
- // createdAt as the order-expiry anchor β see
- // config.payments.orderExpiryHours / refundGraceHours.
- refundAfter DateTime?
-
- // Which facility price list entry priced this intent (issue #127). The
- // amount above is the snapshot; this code records *which* item it was for,
- // so a later price change can be explained without re-deriving it. Null for
- // intents priced from an admin-supplied amount.
- catalogItemCode String?
-
- // Set once funded (create_escrow succeeds) β needed to later call
- // release/refund against the right on-chain escrow.
- escrowId BigInt?
- stellarTxHash String?
- // Which deployed contract this intent was funded against β this repo
- // has already had two testnet contract IDs (Phase E1 vs. E2), so this
- // isn't hypothetical.
- sorobanContractId String?
-
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- @@index([facilityId])
- @@index([status])
- @@index([status, preparedExpiresAt])
-}
-
-// ADR 0005 step 1: a patient's verified Stellar address, used as the payer
-// of a non-custodial escrow. Verification is SEP-10 (a signed challenge),
-// not the type-the-address form used for facilities β but the SEP-10
-// endpoints are not built yet, so nothing in this repo creates these rows
-// today. Prepare (issue #80) requires one and returns 409 WALLET_NOT_LINKED
-// otherwise. Deliberately a separate model from WalletLink: that one is
-// the facility payee (unique per facility), this one is the patient payer
-// (unique per patient), and conflating the two would make a facility's
-// payout address eligible as a patient payer.
-model PatientWalletLink {
- id String @id @default(uuid())
- patientProfileId String @unique
- stellarPublicKey String
- verifiedAt DateTime
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id], onDelete: Cascade)
-}
-
-// SEP-10 (Stellar Web Authentication) challenge issuance ledger β
-// one row per issued challenge, created by GET /payments/sep10/challenge
-// and redeemed exactly once by POST /payments/sep10/verify (issue #77,
-// ADR 0005 step 4: proving the caller controls a Stellar address before
-// it is ever trusted as payer or payee).
-//
-// The nonce is the random 64-byte value embedded in the challenge
-// transaction's `" auth"` Manage Data operation. Storing it
-// (unique) is what makes a challenge single-use: verify consumes the row
-// with ONE conditional UPDATE (`consumedAt IS NULL AND expiresAt > now`),
-// so two concurrent replays can't both win β the same DB-level pattern
-// as booking's conditional slot update (ARCHITECTURE.md Β§5.1).
-//
-// Deliberately no other fields: no XDR, no signature, no PHI. The
-// challenge itself is re-derivable from the submitted XDR at verify
-// time; this table only proves "we issued this nonce to this user for
-// this account, it hasn't been used, and it hasn't expired."
-model Sep10Challenge {
- id String @id @default(uuid())
- userId String
- account String // the G... (or M...) account the challenge was issued for
- nonce String @unique // hex/utf8 nonce from the challenge's Manage Data op
- expiresAt DateTime // matches the challenge's timebounds maxTime (+ record TTL)
- consumedAt DateTime?
- createdAt DateTime @default(now())
-
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
-
- // Pruning and the "is this row still redeemable" check both scan by
- // expiry β same reason LoginAttempt indexes its window. Lookups at
- // verify time go through the unique `nonce` index.
- @@index([expiresAt])
-}
-
-model WalletLink {
- id String @id @default(uuid())
- // Scoped to the facility being paid, not the patient β v1 is a
- // custodial model (docforum-core funds escrows from its own platform
- // Stellar identity; patients never hold or sign with a wallet here).
- // See ADR 0004.
- facilityId String @unique
- facility FacilityProfile @relation(fields: [facilityId], references: [id], onDelete: Cascade)
-
- stellarPublicKey String
-
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-}
-
-// Soroban event ingestion (issue #86). Cursors are per contract deployment;
-// persisted events are immutable observations used for reconciliation/alerts.
-enum EscrowChainEventState {
- pending
- matched
- unmatched
-}
-
-model EscrowEventCursor {
- contractId String @id
- startLedger Int
- cursor String?
- lastLedger Int?
- catchupError String?
- updatedAt DateTime @updatedAt
-}
-
-model EscrowChainEvent {
- id String @id @default(uuid())
- eventId String
- contractId String
- ledger Int
- txHash String?
- escrowId BigInt?
- eventType String
- state EscrowChainEventState @default(pending)
- paymentIntentId String?
- payload Json?
- detectedAt DateTime @default(now())
- alertedAt DateTime?
-
- @@unique([contractId, eventId])
- @@index([state, detectedAt])
- @@index([contractId, ledger])
-}
-
-// --- Audit trail (Phase 8, issue #43) -------------------------------------
-// One row per state transition on a record that matters for clinical,
-// legal or financial accountability (PRD Β§7: "who, when"). Written by the
-// audit module's record(), always inside the same transaction as the
-// change it describes β if the change rolls back, so does its event.
-//
-// APPEND-ONLY. There is no update or delete path in code, and a database
-// trigger (see the add_audit_event migration) rejects UPDATE, DELETE and
-// TRUNCATE outright. Deliberately no foreign keys: an FK's ON DELETE action
-// would have to rewrite or remove audit rows (which the trigger forbids),
-// and RESTRICT would let the audit trail block account lifecycle. Ids are
-// recorded as facts about the past.
-//
-// No PHI, ever: `metadata` is a small flat map of ids/codes validated by
-// record() β never names, contact details, DOB, intake text, or copies of
-// request/response bodies.
-model AuditEvent {
- id String @id @default(uuid())
- // Null only for system-initiated transitions (none exist yet).
- actorUserId String?
- actorRole Role?
- // e.g. 'Appointment', 'DoctorProfile', 'PaymentIntent'.
- entityType String
- entityId String
- // e.g. 'appointment.booked', 'payment_intent.released'.
- action String
- fromState String?
- toState String?
- // X-Request-Id of the HTTP request that caused it (src/middleware/request-id.ts).
- requestId String?
- metadata Json?
- createdAt DateTime @default(now())
-
- @@index([entityType, entityId, createdAt])
- @@index([actorUserId, createdAt])
- @@index([createdAt, id])
-}
+// schema.prisma β DocForum canonical data model
+// SOURCE OF TRUTH: if this file and ARCHITECTURE.md Β§4 disagree, fix ARCHITECTURE.md
+// to match this file (or vice versa) in the same PR β see AGENTS.md.
+//
+// STATUS: Phase 1 models (User/PatientProfile/DoctorProfile/FacilityProfile/
+// Specialty/DoctorSpecialty/FacilityCapability/AvailabilitySlot/Appointment)
+// are fleshed out and migrated. Everything below the Phase 1 marker is still
+// the Phase 0 structural placeholder β see ROADMAP.md.
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+// Phase 8, issue #47: Prisma 7 no longer accepts `url` in the datasource
+// block β the connection is configured in prisma.config.ts (for the CLI:
+// migrate/generate/diff) and passed to PrismaClient as a driver adapter at
+// runtime (see src/db/prisma-client.ts).
+datasource db {
+ provider = "postgresql"
+}
+
+enum Role {
+ patient
+ doctor
+ facility
+ admin
+}
+
+enum VerificationStatus {
+ pending
+ verified
+ rejected
+}
+
+enum FacilityType {
+ lab
+ pharmacy
+}
+
+enum CapabilityType {
+ lab_test
+ medication_dispense
+}
+
+enum SlotStatus {
+ open
+ held
+ booked
+}
+
+enum AppointmentStatus {
+ scheduled
+ completed
+ cancelled
+ no_show
+}
+
+enum AppointmentOrigin {
+ direct_booking
+ referral
+}
+
+enum ConsultationOutcome {
+ resolved
+ referred
+ follow_up_needed
+}
+
+enum ReferralStatus {
+ pending
+ accepted
+ declined
+ completed
+ expired
+ // Admin-initiated termination (issue #68) β distinct from `expired`
+ // (the cascade gave up after repeated no-shows) and `declined` (the
+ // patient said no) so audit/metrics can tell the three exits apart.
+ cancelled
+}
+
+// Why a referral reached the terminal `expired` state. Kept separate from
+// `status` so `expired` stays one state (and stays distinct from `declined`
+// and `cancelled`) while audit/metrics/notifications can still tell the two
+// ways in apart: `ttl` = nobody accepted/declined it in time (issue #8),
+// `no_show` = the cascade gave up after repeated no-shows (issue #67).
+enum ReferralExpiryReason {
+ ttl
+ no_show
+}
+
+enum OrderStatus {
+ issued
+ fulfilled
+ expired
+ superseded
+}
+
+enum OrderType {
+ pharmacy
+ lab
+}
+
+enum DisputeStatus {
+ pending
+ resolved
+}
+
+enum FulfillmentStatus {
+ pending
+ in_progress
+ fulfilled
+ rejected
+}
+
+enum Urgency {
+ routine
+ urgent
+}
+
+// =========================================================================
+// Phase 1 models β identity, DB, safe booking foundation. See ROADMAP.md.
+// =========================================================================
+
+model User {
+ id String @id @default(uuid())
+ email String @unique
+ phone String?
+ passwordHash String
+ role Role
+ // Phase 8 (issue #34): null = never verified. Self-serve signup leaves
+ // this null; existing rows were backfilled to `createdAt` in the
+ // add_email_verification migration. Distinct from
+ // DoctorProfile.verificationStatus, which is the separate, manual
+ // admin verification of a doctor's credentials β do not conflate.
+ emailVerifiedAt DateTime?
+ // Phase 9, issue #133: server-generated text preference. Values are
+ // normalized to a supported language tag at signup; unsupported tags
+ // fall back to English.
+ locale String @default("en")
+ // Phase 8 (issue #39): null = active. Deactivating never deletes anything
+ // β no clinical or payment record is keyed to this flag, and none of the
+ // deactivation paths remove rows. Login, refresh and access tokens are all
+ // refused while this is set.
+ deactivatedAt DateTime?
+ // Phase 8 (issue #39): bumped when an account is deactivated/reactivated,
+ // so access tokens minted before that moment stop working immediately
+ // instead of living out their 15-minute TTL. There is still no general
+ // refresh-token revocation list (issue #18) β this is the narrow "revoke
+ // everything for one account" case that deactivation needs.
+ sessionsInvalidatedAt DateTime?
+ // Per-user refresh-token version. Embedded in every access/refresh token
+ // as `tv` and compared against this column at refresh time. Bumping it
+ // (password reset, Phase 8 #33) invalidates every outstanding refresh
+ // token for the user, since none of them carry the new value β the
+ // interim kill switch issue #33 asked for while #18 doesn't exist yet.
+ tokenVersion Int @default(0)
+ // Phase 8 (issue #41, docs/adr/0010): set once, irreversibly, when the
+ // account is deleted. The row is anonymised in place (email tombstoned,
+ // phone cleared, password unusable) rather than deleted, because clinical
+ // and payment records still reference its patient profile. Refresh is
+ // refused once set. Distinct from admin deactivation (#39), which is
+ // reversible.
+ deletedAt DateTime?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ patientProfile PatientProfile?
+ doctorProfile DoctorProfile?
+ facilityProfile FacilityProfile?
+ refreshSessions RefreshSession[]
+
+ emailVerificationTokens EmailVerificationToken[]
+ passwordResetTokens PasswordResetToken[]
+ adminInvitesSent AdminInvite[]
+ // Phase 8 (issue #45): PHI read-access log rows this user wrote (as viewer).
+ // Opposite of AccessLog.accessedByUser.
+ accessLogs AccessLog[]
+ // Phase 8 (issue #42, docs/adr/0011): dependents this user is guardian of.
+ guardianships Guardianship[]
+ orderDisputeResolutions OrderDisputeResolution[]
+ sep10Challenges Sep10Challenge[]
+
+ @@index([role])
+}
+
+// Phase 8 (issue #34): hashed, single-use, expiring email-verification
+// tokens. Only the SHA-256 hash is persisted β a database leak must not
+// hand out working verification links. One row per issued token so a
+// resend can be rate-limited against the most recent `createdAt`.
+model EmailVerificationToken {
+ id String @id @default(uuid())
+ userId String
+ tokenHash String @unique
+ expiresAt DateTime
+ consumedAt DateTime?
+ createdAt DateTime @default(now())
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@index([userId])
+}
+
+// Password reset tokens (Phase 8 #33). Only the SHA-256 hash of the
+// token is ever stored β the raw token exists exactly once, in the
+// notification that carries the reset link, and is never logged or
+// persisted. Single-use (`usedAt`) and expiring (`expiresAt`).
+model PasswordResetToken {
+ id String @id @default(uuid())
+ userId String
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ // SHA-256 hex of the raw token. Unique so lookup-by-hash is a single
+ // indexed read, and a duplicate hash (astronomically unlikely) is a
+ // constraint violation rather than a silent overwrite.
+ tokenHash String @unique
+ expiresAt DateTime
+ // Non-null once used or superseded by a newer request β either way the
+ // token is dead. Requesting a second reset marks the first one used.
+ usedAt DateTime?
+ createdAt DateTime @default(now())
+
+ @@index([userId])
+}
+
+model AdminInvite {
+ id String @id @default(uuid())
+ email String
+ tokenHash String @unique
+ expiresAt DateTime
+ consumedAt DateTime?
+ invitedByUserId String
+ createdAt DateTime @default(now())
+
+ invitedBy User @relation(fields: [invitedByUserId], references: [id], onDelete: Cascade)
+
+ @@index([email])
+}
+
+// Phase 8 (issue #35): login-throttle state. Deliberately in Postgres and
+// not in process memory β the backend runs as a single Render instance
+// today but must not assume that forever, and a restart must not hand an
+// attacker a fresh budget. One row per (scope, key) bucket, where scope is
+// `email` (normalised address) or `ip` (client IP, after the proxy chain is
+// resolved β see TRUST_PROXY in src/config/index.ts).
+//
+// Counters are only ever moved by a single conditional upsert (see
+// auth/repositories `recordFailedLoginAttempt`), never read-then-write: two
+// concurrent bad logins for the same account must both be counted.
+model LoginAttempt {
+ id String @id @default(uuid())
+ scope String // 'email' | 'ip'
+ key String // normalised email, or client IP
+ failedCount Int @default(0)
+ // Start of the current sliding window; a failure outside the window
+ // restarts the count at 1 instead of accumulating forever.
+ windowStart DateTime @default(now())
+ lastFailedAt DateTime @default(now())
+ // Set once this bucket has hit the lock threshold; cleared by a
+ // successful login, an admin clear, or the start of a new window.
+ lockUntil DateTime?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ @@unique([scope, key])
+ @@index([lastFailedAt])
+}
+
+// One row per refresh-token *family*, i.e. per login. This is the
+// revocation store session management needs (Phase 8 #38) and the shape
+// issue #18's refresh-token revocation wanted: the refresh JWT carries
+// `sid`, and a refresh is only honoured while the row exists with
+// `revokedAt` still null.
+//
+// Collecting a coarse IP and user agent here is deliberate β it is what
+// makes "sign out that other device" usable. Nothing clinical or
+// identifying beyond what the request itself already carried.
+model RefreshSession {
+ id String @id @default(uuid())
+ userId String
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ createdAt DateTime @default(now())
+ // Updated on refresh only β never on every access-token-authenticated
+ // request, so this doesn't become a write hot-spot on the User row.
+ lastUsedAt DateTime @default(now())
+ revokedAt DateTime?
+
+ userAgent String?
+ ip String?
+
+ @@index([userId])
+ @@index([userId, revokedAt])
+}
+
+model PatientProfile {
+ id String @id @default(uuid())
+ // Null for a dependent (Phase 8, issue #42, docs/adr/0011): a
+ // guardian-owned profile with no login of its own. Still unique, so a
+ // login owns at most one profile. A DB CHECK (see the
+ // guardian_dependent_profiles migration) requires dateOfBirth whenever
+ // userId is null β the age-of-majority rule depends on it.
+ userId String? @unique
+ fullName String
+ dateOfBirth DateTime?
+ // Phase 8 (issue #41, docs/adr/0010): set when the owner deleted their
+ // account. fullName/dateOfBirth are anonymised at the same moment; the row
+ // itself stays so appointments and payments keep their foreign key.
+ // getMyPatientProfile refuses an anonymised profile.
+ anonymizedAt DateTime?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // RESTRICT, not CASCADE (issue #41): a patient's User row is never
+ // hard-deleted while this profile exists β deletion is anonymisation
+ // (ADR 0010), and an accidental user delete must fail loudly rather than
+ // take the clinical identity anchor with it. Optional since issue #42: a
+ // dependent profile has no login.
+ user User? @relation(fields: [userId], references: [id], onDelete: Restrict)
+ appointments Appointment[]
+ paymentIntents PaymentIntent[]
+ prescriptions Prescription[]
+ labOrders LabOrder[]
+ dataExports PatientExport[]
+ // Phase 3: the referrals this patient is the subject of (the opposite
+ // side of Referral.patientProfileId).
+ referrals Referral[]
+ // ADR 0005 step 1 (issue #80): the patient's verified Stellar payer
+ // address, when one has been linked. Optional because linking is a
+ // separate flow that nothing creates rows for yet.
+ walletLink PatientWalletLink?
+
+ // Phase 8 (issue #45): who viewed this patient's records.
+ accessLogs AccessLog[]
+
+ // Phase 8 (issue #42, docs/adr/0011): guardians of this (dependent) profile.
+ guardians Guardianship[]
+ orders Order[]
+ orderDisputes OrderDispute[]
+}
+
+// Phase 8 (issue #40): right-of-access export jobs. One row per request;
+// the generated bundle itself lives in PatientExportChunk (below), not in
+// this row. Lifecycle: pending -> processing -> ready -> expired, or
+// -> failed. A job only moves forward through conditional UPDATEs
+// (`WHERE status = ...`), the same shape as the booking guarantee, so two
+// workers can never both generate one export.
+enum PatientExportStatus {
+ pending
+ processing
+ ready
+ failed
+ expired
+}
+
+model PatientExport {
+ id String @id @default(uuid())
+ // The owner. Every read is scoped by this, never by the export id alone.
+ patientProfileId String
+ status PatientExportStatus @default(pending)
+ // Bundle format version actually written (set when generation starts).
+ schemaVersion Int?
+ // Fencing token: incremented on every claim. Chunks are keyed by it, and
+ // the final `ready` transition is conditional on it, so a worker whose
+ // claim went stale can never publish a half-written or superseded bundle.
+ attempt Int @default(0)
+ startedAt DateTime?
+ generatedAt DateTime?
+ // Set when the job becomes `ready`; downloads are refused at or after it
+ // even if the cleanup job hasn't run yet.
+ expiresAt DateTime?
+ sizeBytes Int?
+ // How many chunks the published attempt wrote. A download that finds
+ // fewer (cleanup ran mid-stream) aborts instead of handing out a
+ // silently truncated JSON document.
+ chunkCount Int?
+ // Machine-readable failure only (e.g. SECTION_FAILED + section name) β
+ // never an exception message, which could echo patient data.
+ failureCode String?
+ failedSection String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id], onDelete: Cascade)
+ chunks PatientExportChunk[]
+
+ @@index([patientProfileId, createdAt])
+ @@index([status, expiresAt])
+}
+
+// The export bundle, stored as ordered text chunks of one JSON document
+// (issue #40). Written incrementally while sections are paged, read back
+// one chunk at a time on download, so neither side ever holds a whole
+// patient history in memory. Kept in Postgres rather than object storage
+// because no object-storage provider is integrated yet (ARCHITECTURE.md Β§2
+// and Β§6.3.4) β only patients/repositories reads or writes it, so moving to
+// S3 later replaces that storage seam, not the export design.
+model PatientExportChunk {
+ exportId String
+ attempt Int
+ seq Int
+ data String
+
+ export PatientExport @relation(fields: [exportId], references: [id], onDelete: Cascade)
+
+ @@id([exportId, attempt, seq])
+}
+
+// Phase 8 (issue #42, docs/adr/0011): replaces the unenforced
+// PatientProfile.guardianUserId string. A guardian (a patient User) owns a
+// dependent PatientProfile they created; there is deliberately no way to
+// link an existing profile. Active = revokedAt is null AND the dependent is
+// under AGE_OF_MAJORITY_YEARS (checked at request time from the DOB β no
+// job flips anything). Never deleted: ending a guardianship sets revokedAt,
+// so who had access, and when, stays on record.
+model Guardianship {
+ id String @id @default(uuid())
+ guardianUserId String
+ dependentProfileId String
+ createdAt DateTime @default(now())
+ revokedAt DateTime?
+
+ guardian User @relation(fields: [guardianUserId], references: [id], onDelete: Restrict)
+ dependentProfile PatientProfile @relation(fields: [dependentProfileId], references: [id], onDelete: Restrict)
+
+ // One row per (guardian, dependent) pair β no duplicate or ambiguous
+ // relationships. More than one guardian per dependent is allowed by the
+ // model (two parents), though v1's API only creates one.
+ @@unique([guardianUserId, dependentProfileId])
+ @@index([dependentProfileId])
+}
+
+model DoctorProfile {
+ id String @id @default(uuid())
+ userId String @unique
+ fullName String
+ verificationStatus VerificationStatus @default(pending)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ specialties DoctorSpecialty[]
+ availabilitySlots AvailabilitySlot[]
+ appointments Appointment[]
+ prescriptions Prescription[]
+ labOrders LabOrder[]
+ // Referral sides a doctor can sit on: `referringReferrals` are ones
+ // they created; `targetedReferrals` are ones aimed at them (whether
+ // they ultimately see the patient or not).
+ referringReferrals Referral[] @relation("ReferralReferringDoctor")
+ targetedReferrals Referral[] @relation("ReferralTargetDoctor")
+
+ @@index([verificationStatus])
+}
+
+model Specialty {
+ id String @id @default(uuid())
+ name String @unique
+ slug String @unique
+
+ doctors DoctorSpecialty[]
+}
+
+model DoctorSpecialty {
+ id String @id @default(uuid())
+ doctorProfileId String
+ specialtyId String
+
+ doctorProfile DoctorProfile @relation(fields: [doctorProfileId], references: [id], onDelete: Cascade)
+ specialty Specialty @relation(fields: [specialtyId], references: [id], onDelete: Cascade)
+
+ @@unique([doctorProfileId, specialtyId])
+}
+
+model FacilityProfile {
+ id String @id @default(uuid())
+ userId String @unique
+ name String
+ type FacilityType
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ capabilities FacilityCapability[]
+ prescriptions Prescription[]
+ labOrders LabOrder[]
+ labResults LabResult[]
+ paymentIntents PaymentIntent[]
+ fulfillments FulfillmentRecord[]
+ walletLink WalletLink?
+ prices FacilityPrice[]
+ orders Order[]
+ fulfillmentProofs FulfillmentProof[]
+}
+
+model FacilityCapability {
+ id String @id @default(uuid())
+ facilityProfileId String
+ capabilityType CapabilityType
+ // e.g. a lab test category, or a medication class β free text on purpose,
+ // no controlled vocabulary decided yet (out of Phase 1 scope).
+ detail String?
+
+ facilityProfile FacilityProfile @relation(fields: [facilityProfileId], references: [id], onDelete: Cascade)
+
+ @@unique([facilityProfileId, capabilityType, detail])
+}
+
+/// Versioned facility price list (issue #127). Rows are append-only: changing
+/// a price inserts a new row with a later `effectiveFrom`, so history is kept
+/// and an intent priced earlier keeps resolving against what was in force at
+/// its selection time. Amounts are stroops (BigInt) like PaymentIntent.
+///
+/// `catalogItemCode` is deliberately an open vocabulary (a lab test or
+/// medication code the facility publishes), matching FacilityCapability.detail
+/// β no controlled vocabulary has been decided yet.
+model FacilityPrice {
+ id String @id @default(uuid())
+ facilityId String
+ facility FacilityProfile @relation(fields: [facilityId], references: [id], onDelete: Cascade)
+ catalogItemCode String
+ // Stellar asset the price is denominated in. v1 quotes native XLM
+ // ('native'); the column exists so a price list can add assets without a
+ // schema change when multi-asset payments land.
+ asset String
+ amount BigInt
+ effectiveFrom DateTime
+ createdAt DateTime @default(now())
+
+ // Two versions of the same item/asset cannot share an effective instant.
+ @@unique([facilityId, catalogItemCode, asset, effectiveFrom])
+ @@index([facilityId, catalogItemCode, asset, effectiveFrom])
+ @@index([catalogItemCode, asset, effectiveFrom])
+}
+
+model AvailabilitySlot {
+ id String @id @default(uuid())
+ doctorProfileId String
+ status SlotStatus @default(open)
+ startTime DateTime
+ endTime DateTime
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ doctorProfile DoctorProfile @relation(fields: [doctorProfileId], references: [id], onDelete: Cascade)
+ appointment Appointment?
+
+ // Prevents two slot ROWS for the same doctor/time range (duplicate
+ // generation). This is NOT what prevents double-BOOKING of one slot by
+ // two patients β that's the conditional `UPDATE ... WHERE status =
+ // 'open'` in availability/services, inside a transaction. See
+ // ARCHITECTURE.md Β§5.1 and the integration test in
+ // backend/tests/integration/booking.test.ts.
+ @@unique([doctorProfileId, startTime, endTime])
+ @@index([doctorProfileId, status])
+}
+
+model Appointment {
+ id String @id @default(uuid())
+ patientProfileId String
+ doctorProfileId String
+ // Nullable because a CANCELLED appointment releases its slot back to
+ // `open` (referral cascade / patient cancel) β the appointment keeps its
+ // history row but must stop holding the unique claim on the slot, or
+ // re-booking the freed slot would violate the unique constraint.
+ // The booking guarantee itself is unchanged: claimOpenSlot's
+ // conditional UPDATE on SlotStatus is what prevents double-booking.
+ availabilitySlotId String? @unique
+ status AppointmentStatus @default(scheduled)
+ origin AppointmentOrigin @default(direct_booking)
+ // Set when this appointment was produced by accepting a Referral (issue
+ // #7). The cascade rules (#67 / PRD Β§8.2.3) react to status transitions
+ // on exactly these appointments. NOT unique: one referral can produce
+ // several appointments over its life (re-booking after a cancellation).
+ // Referral.resultingAppointmentId is the unique "current" pointer.
+ referralId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // Explicit RESTRICT (was the Prisma default β same SQL): an appointment is
+ // a clinical record and must outlive any account deletion (ADR 0010).
+ patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id], onDelete: Restrict)
+ doctorProfile DoctorProfile @relation(fields: [doctorProfileId], references: [id])
+ availabilitySlot AvailabilitySlot? @relation(fields: [availabilitySlotId], references: [id])
+ referral Referral? @relation("ReferralOriginatedAppointments", fields: [referralId], references: [id])
+ // Non-owning side of Referral.resultingAppointmentId (the FK lives on
+ // Referral) β which referral currently points at this appointment as
+ // the one it produced.
+ referralResultOf Referral? @relation("ReferralResultingAppointment")
+ // Phase 2 (issue #4): the appointment's consultation. 1:1 β the uniqueness
+ // is Consultation.appointmentId UNIQUE (docs/adr/0009), so this side is a
+ // plain back-relation.
+ consultation Consultation?
+
+ // TODO Phase 2: intakeId (FK -> IntakeForm). PRD FR-5 requires intake
+ // before confirmation, not enforced yet β booking works without it in
+ // Phase 1 so the concurrency-safety work isn't blocked on IntakeForm
+ // existing. Flagged, not silently dropped.
+}
+
+// =========================================================================
+model AccessLog {
+ id String @id @default(uuid())
+ // Who viewed. SetNull on user delete keeps the log row (timestamp +
+ // entity remain) while removing the PII pointer.
+ accessedBy String?
+ // Which patient's records were viewed (the record owner, not the viewer).
+ patientProfileId String?
+ // Entity that was read: 'IntakeForm' | 'Consultation' | 'Prescription' |
+ // 'LabOrder' | 'LabResult' | 'Referral' | 'PaymentIntent' | 'PatientProfile'
+ entityType String
+ entityId String
+ // The HTTP route that returned the payload (for correlating with access logs).
+ route String?
+ // Free-text detail β no PHI here, just context like "GET /appointments/mine".
+ detail String?
+ accessedAt DateTime @default(now())
+
+ accessedByUser User? @relation(fields: [accessedBy], references: [id], onDelete: SetNull)
+ patientProfile PatientProfile? @relation(fields: [patientProfileId], references: [id], onDelete: Cascade)
+
+ @@index([accessedAt])
+ @@index([accessedBy])
+ @@index([patientProfileId, accessedAt])
+ @@index([entityType, entityId])
+}
+
+// =========================================================================
+// Orders, Fulfillment Proofs, Disputes & Resolutions
+// =========================================================================
+
+model Order {
+ id String @id @default(uuid())
+ orderType OrderType
+ patientProfileId String
+ facilityProfileId String?
+ prescribingDoctorId String?
+ details String?
+ status OrderStatus @default(issued)
+
+ fulfilledAt DateTime?
+
+ confirmed Boolean @default(false)
+ confirmationSource String? // 'patient' | 'timeout'
+ confirmedAt DateTime?
+
+ disputed Boolean @default(false)
+ disputedAt DateTime?
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id])
+ facilityProfile FacilityProfile? @relation(fields: [facilityProfileId], references: [id])
+
+ fulfillmentProofs FulfillmentProof[]
+ disputes OrderDispute[]
+
+ @@index([patientProfileId, status])
+ @@index([facilityProfileId, status])
+ @@index([status, confirmed, disputed])
+}
+
+model FulfillmentProof {
+ id String @id @default(uuid())
+ orderId String
+ facilityProfileId String
+ referenceNumber String
+ attachmentUrl String?
+ notes String?
+ createdAt DateTime @default(now())
+
+ order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
+ facilityProfile FacilityProfile @relation(fields: [facilityProfileId], references: [id])
+
+ @@index([orderId])
+ @@index([facilityProfileId])
+}
+
+model OrderDispute {
+ id String @id @default(uuid())
+ orderId String
+ patientProfileId String
+ reason String
+ status DisputeStatus @default(pending)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
+ patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id])
+ resolutions OrderDisputeResolution[]
+
+ @@index([orderId])
+ @@index([patientProfileId])
+ @@index([status])
+}
+
+model OrderDisputeResolution {
+ id String @id @default(uuid())
+ disputeId String
+ adminUserId String
+ resolutionNotes String
+ outcome String // e.g. 'upheld' | 'rejected' | 'refunded'
+ createdAt DateTime @default(now())
+
+ dispute OrderDispute @relation(fields: [disputeId], references: [id], onDelete: Cascade)
+ adminUser User @relation(fields: [adminUserId], references: [id])
+
+ @@index([disputeId])
+ @@index([adminUserId])
+}
+
+// Below this line: Phase 0 structural placeholders, not yet implemented.
+// See ROADMAP.md for phase order.
+// =========================================================================
+
+model IntakeForm {
+ id String @id @default(uuid())
+ createdAt DateTime @default(now())
+ // TODO: patientProfileId, symptoms (Json), freeTextNotes
+}
+
+// Phase 3 (issue #5 minimal + lineage for #65): a Consultation is the
+// record of a doctor having seen the patient at a specific appointment.
+// `appointmentId` is the lineage seam β Referral.parentReferralId is
+// derived through it (consultation β appointment β referral), which is
+// what loop detection walks. Full consultation-note workflow (append-only
+// addenda, sign-off) stays issue #4/#64's scope.
+model Consultation {
+ id String @id @default(uuid())
+ // unique = "a consultation is closed once per appointment", enforced by the
+ // database rather than by a read-then-write check (docs/adr/0009).
+ appointmentId String @unique
+ // Nullable so a consultation can exist before sign-off (Phase 3's referral
+ // slice); the close routes require it, so a closed record always has one.
+ outcome ConsultationOutcome?
+ // Clinical free text: stored as an AES-256-GCM envelope by
+ // consultations/services (issue #46 β ARCHITECTURE.md Β§4.3), never plaintext.
+ notes String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // RESTRICT (Prisma's default for a required relation) is what the existing
+ // migration created: a clinical record must not disappear because an
+ // appointment row was deleted.
+ appointment Appointment @relation(fields: [appointmentId], references: [id])
+ prescriptions Prescription[]
+ labOrders LabOrder[]
+ // Phase 3: the referrals that grew out of this consultation (the
+ // opposite side of Referral.sourceConsultationId).
+ referrals Referral[]
+}
+
+// Phase 3 (issues #5/#65/#66/#67/#68). Snapshot rule (FR-7/FR-8, hard
+// rule 3): `snapshotNotes` is a copy taken at creation, never a live
+// reference β reassign/cancel/cascade never touch it.
+model Referral {
+ id String @id @default(uuid())
+ status ReferralStatus @default(pending)
+ patientProfileId String
+ // The consultation this referral grew out of. Required: every referral
+ // is created from a consultation (PRD FR-7), which is also what gives
+ // loop detection its lineage.
+ sourceConsultationId String
+ referringDoctorId String
+ // Always a concrete doctor: a "specialty, no specific doctor" referral
+ // is routed to one at creation (least-loaded eligible doctor β issue
+ // #6, docs/adr/0008). The column stays NOT NULL on purpose, so an
+ // unassigned referral cannot exist; when nobody is eligible the
+ // creation fails instead of writing a row.
+ targetDoctorId String
+ // Optional denormalised label of the target specialty, used by the
+ // loop guard and admin views. Not a substitute for issue #23's real
+ // per-doctor specialty management.
+ targetSpecialtyId String?
+ snapshotNotes String
+ // Issue #65: the referral whose appointment produced the consultation
+ // this referral came from β the chain loop detection walks (bounded).
+ parentReferralId String?
+ // Issue #67 bookkeeping: how many times a referral-origin appointment
+ // has no-showed, and the "the referring doctor must know" flag set
+ // when repeated no-shows expire the referral. Both live on the
+ // referral (not a Notification row) because Phase 6's dispatch layer
+ // doesn't exist yet β the flag is the seam it will read.
+ noShowCount Int @default(0)
+ referringDoctorNotifiedAt DateTime?
+ // Issue #65: set (with a reason naming the matching hop) when the
+ // target doctor/specialty was already in the ancestor chain β the
+ // referral is still created (a human decides), but surfaces in the
+ // admin queue.
+ loopFlaggedAt DateTime?
+ loopFlagReason String?
+ // Issue #68: admin-initiated cancellation carries its reason; the
+ // audit trail proper is the AuditEvent row written alongside.
+ cancelledReason String?
+ // The appointment this referral currently produced (null while
+ // pending/declined, or after a cancelled/no-showed appointment returns
+ // it to pending). Kept pointing at the last no-show when expired β
+ // that appointment is why it expired.
+ resultingAppointmentId String? @unique
+ // Issue #8: set together with status `expired` β when, and why.
+ expiredAt DateTime?
+ expiredReason ReferralExpiryReason?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ sourceConsultation Consultation @relation(fields: [sourceConsultationId], references: [id])
+ patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id])
+ referringDoctor DoctorProfile @relation("ReferralReferringDoctor", fields: [referringDoctorId], references: [id])
+ targetDoctor DoctorProfile @relation("ReferralTargetDoctor", fields: [targetDoctorId], references: [id])
+ parentReferral Referral? @relation("ReferralLineage", fields: [parentReferralId], references: [id])
+ childReferrals Referral[] @relation("ReferralLineage")
+ resultingAppointment Appointment? @relation("ReferralResultingAppointment", fields: [resultingAppointmentId], references: [id])
+ // Every appointment this referral ever produced (including cancelled/
+ // no-showed ones) β history survives the resultingAppointmentId link
+ // being cleared when the referral returns to pending.
+ appointments Appointment[] @relation("ReferralOriginatedAppointments")
+
+ @@index([patientProfileId])
+ @@index([referringDoctorId])
+ @@index([targetDoctorId])
+ @@index([status])
+ @@index([status, updatedAt])
+ @@index([loopFlaggedAt])
+}
+
+// Issue #68: who did what to a referral, when, and why. Deliberately NOT a
+// second audit table: the append-only `AuditEvent` in this schema (Phase 8,
+// issue #43) is the one audit trail, so referral admin actions are recorded
+// as `entityType: 'Referral'` rows through the audit module
+// (`auditService.record`) β same table, same triggers, same writer.
+
+model Prescription {
+ id String @id @default(uuid())
+ consultationId String
+ patientProfileId String
+ prescribingDoctorId String
+ facilityProfileId String
+ amountStroops BigInt
+ status OrderStatus @default(issued)
+ supersedesPrescriptionId String?
+ createdAt DateTime @default(now())
+
+ consultation Consultation @relation(fields: [consultationId], references: [id])
+ patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id])
+ prescribingDoctor DoctorProfile @relation(fields: [prescribingDoctorId], references: [id])
+ facilityProfile FacilityProfile @relation(fields: [facilityProfileId], references: [id])
+ supersedes Prescription? @relation("PrescriptionCorrections", fields: [supersedesPrescriptionId], references: [id])
+ correctedBy Prescription? @relation("PrescriptionCorrections")
+ items PrescriptionItem[]
+ paymentIntent PaymentIntent?
+ fulfillment FulfillmentRecord?
+
+ @@unique([supersedesPrescriptionId])
+ @@index([patientProfileId, createdAt])
+ @@index([consultationId])
+}
+
+model PrescriptionItem {
+ id String @id @default(uuid())
+ prescriptionId String
+ drugName String
+ dosage String
+ frequency String
+ duration String
+
+ prescription Prescription @relation(fields: [prescriptionId], references: [id])
+
+ @@index([prescriptionId])
+}
+
+model LabOrder {
+ id String @id @default(uuid())
+ consultationId String
+ patientProfileId String
+ orderingDoctorId String
+ facilityProfileId String
+ amountStroops BigInt
+ testType String
+ urgency Urgency @default(routine)
+ notes String?
+ status OrderStatus @default(issued)
+ supersedesLabOrderId String?
+ createdAt DateTime @default(now())
+
+ consultation Consultation @relation(fields: [consultationId], references: [id])
+ patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id])
+ orderingDoctor DoctorProfile @relation(fields: [orderingDoctorId], references: [id])
+ facilityProfile FacilityProfile @relation(fields: [facilityProfileId], references: [id])
+ supersedes LabOrder? @relation("LabOrderCorrections", fields: [supersedesLabOrderId], references: [id])
+ correctedBy LabOrder? @relation("LabOrderCorrections")
+ result LabResult?
+ paymentIntent PaymentIntent?
+ fulfillment FulfillmentRecord?
+
+ @@unique([supersedesLabOrderId])
+ @@index([patientProfileId, createdAt])
+ @@index([consultationId])
+}
+
+model LabResult {
+ id String @id @default(uuid())
+ labOrderId String @unique
+ fulfillingFacilityId String
+ resultFileUrl String?
+ summary String?
+ createdAt DateTime @default(now())
+
+ labOrder LabOrder @relation(fields: [labOrderId], references: [id])
+ fulfillingFacility FacilityProfile @relation(fields: [fulfillingFacilityId], references: [id])
+}
+
+/// Facility-side fulfillment state for one issued order. Kept separate from
+/// the order so a facility never needs write access to the clinical record
+/// (ARCHITECTURE.md Β§4.7, hard rule 5).
+///
+/// Issue #13 / docs/adr/0012: the Phase 0 sketch here was polymorphic
+/// (`orderType` + `orderId` TEXT). Both are replaced by real FKs β exactly one
+/// of them is set, required by a PostgreSQL CHECK, and the UNIQUE on each
+/// means an order can have at most one fulfillment row. Same discriminator
+/// mechanism `PaymentIntent` adopted in issue #85.
+///
+/// `facilityId` is copied from the order's own facility at creation (the
+/// creating service in Phase 5 issue #14 owns that rule) so the facility's
+/// queue is indexable on this table and ownership lives on the row the
+/// facility writes.
+model FulfillmentRecord {
+ id String @id @default(uuid())
+ prescriptionId String? @unique
+ labOrderId String? @unique
+ facilityId String
+ status FulfillmentStatus @default(pending)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ prescription Prescription? @relation(fields: [prescriptionId], references: [id], onDelete: Restrict)
+ labOrder LabOrder? @relation(fields: [labOrderId], references: [id], onDelete: Restrict)
+ facility FacilityProfile @relation(fields: [facilityId], references: [id], onDelete: Restrict)
+
+ @@index([facilityId, status])
+}
+
+model Notification {
+ id String @id @default(uuid())
+ type String
+ payload Json
+ readAt DateTime?
+ createdAt DateTime @default(now())
+ // TODO: userId
+}
+
+// --- Stellar payments (docforum-org split) -------------------------------
+// These two models hold relational metadata ONLY β amounts, status, tx
+// hashes, public keys. No clinical/PHI field ever belongs here. The actual
+// escrow contract logic lives in the sibling docforum-escrow repo and is
+// called via the @docforum/escrow-sdk package from
+// backend/src/modules/payments/services β never modeled or reimplemented
+// in this schema. See docs/adr/0002-stellar-escrow-for-fulfillment-payout.md
+// and docs/adr/0004-custodial-payments-v1.md.
+//
+// One of prescriptionId/labOrderId is required by a PostgreSQL check
+// constraint. Amount and recipient are snapshotted from the issued order.
+
+enum PaymentIntentStatus {
+ created
+ // ADR 0005 steps 2-4: a prepared (unsigned) create_escrow transaction
+ // exists, then is being submitted with the patient's signature. The
+ // submit half is not built yet (issue #80 only covers prepare), so
+ // `submitting` is reserved by the state machine but not written today.
+ awaiting_signature
+ submitting
+ escrowed
+ released
+ refunded
+ failed
+}
+
+model PaymentIntent {
+ id String @id @default(uuid())
+ prescriptionId String? @unique
+ labOrderId String? @unique
+ status PaymentIntentStatus @default(created)
+
+ prescription Prescription? @relation(fields: [prescriptionId], references: [id])
+ labOrder LabOrder? @relation(fields: [labOrderId], references: [id])
+
+ patientProfileId String
+ // Explicit RESTRICT (was the Prisma default β same SQL): a payment record
+ // must outlive any account deletion (ADR 0010).
+ patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id], onDelete: Restrict)
+ facilityId String
+ facility FacilityProfile @relation(fields: [facilityId], references: [id])
+
+ // Amount in the smallest unit of `assetCode` (stroops for XLM, 7-decimal
+ // units for USDC) β matches @docforum/escrow-sdk's `amount: bigint`
+ // exactly. Renamed from `amountStroops` by issue #79; the migration is a
+ // plain column rename, so existing rows keep their value. This module
+ // never scales or converts amounts, so it never assumes an asset's
+ // decimals (the allowlist carries them for clients only).
+ amount BigInt
+ // The asset this intent is denominated in β `XLM` or `USDC` in the
+ // default testnet allowlist. Assets are identified on-chain by the SAC
+ // contract id, not by the code, so a new intent must name an allowlisted
+ // asset (see config.payments.assets and issue #79).
+ assetCode String
+ // The Stellar Asset Contract address the escrow pulls from. Nullable only
+ // for rows created before issue #79: those were funded against
+ // `ESCROW_TOKEN_ID` (native XLM), which the service falls back to when it
+ // sees a null here. New rows always store it.
+ tokenContractId String?
+
+ // ADR 0005 step 2 (issue #80). `preparedTxHash` is the hash of the
+ // unsigned create_escrow transaction the patient must sign;
+ // `preparedXdr` lets a retry return exactly the same transaction instead
+ // of building a second one. Both are cleared when the preparation
+ // expires back to `created`.
+ preparedTxHash String?
+ preparedXdr String?
+ preparedExpiryLedger Int?
+ preparedExpiresAt DateTime?
+ // ADR 0005: escrow refund deadline = order expiry + grace. Orders don't
+ // exist as real records yet (Phase 4), so the service uses the intent's
+ // createdAt as the order-expiry anchor β see
+ // config.payments.orderExpiryHours / refundGraceHours.
+ refundAfter DateTime?
+
+ // Which facility price list entry priced this intent (issue #127). The
+ // amount above is the snapshot; this code records *which* item it was for,
+ // so a later price change can be explained without re-deriving it. Null for
+ // intents priced from an admin-supplied amount.
+ catalogItemCode String?
+
+ // Set once funded (create_escrow succeeds) β needed to later call
+ // release/refund against the right on-chain escrow.
+ escrowId BigInt?
+ stellarTxHash String?
+ // Which deployed contract this intent was funded against β this repo
+ // has already had two testnet contract IDs (Phase E1 vs. E2), so this
+ // isn't hypothetical.
+ sorobanContractId String?
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ @@index([facilityId])
+ @@index([status])
+ @@index([status, preparedExpiresAt])
+}
+
+// ADR 0005 step 1: a patient's verified Stellar address, used as the payer
+// of a non-custodial escrow. Verification is SEP-10 (a signed challenge),
+// not the type-the-address form used for facilities β but the SEP-10
+// endpoints are not built yet, so nothing in this repo creates these rows
+// today. Prepare (issue #80) requires one and returns 409 WALLET_NOT_LINKED
+// otherwise. Deliberately a separate model from WalletLink: that one is
+// the facility payee (unique per facility), this one is the patient payer
+// (unique per patient), and conflating the two would make a facility's
+// payout address eligible as a patient payer.
+model PatientWalletLink {
+ id String @id @default(uuid())
+ patientProfileId String @unique
+ stellarPublicKey String
+ verifiedAt DateTime
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ patientProfile PatientProfile @relation(fields: [patientProfileId], references: [id], onDelete: Cascade)
+}
+
+// SEP-10 (Stellar Web Authentication) challenge issuance ledger β
+// one row per issued challenge, created by GET /payments/sep10/challenge
+// and redeemed exactly once by POST /payments/sep10/verify (issue #77,
+// ADR 0005 step 4: proving the caller controls a Stellar address before
+// it is ever trusted as payer or payee).
+//
+// The nonce is the random 64-byte value embedded in the challenge
+// transaction's `" auth"` Manage Data operation. Storing it
+// (unique) is what makes a challenge single-use: verify consumes the row
+// with ONE conditional UPDATE (`consumedAt IS NULL AND expiresAt > now`),
+// so two concurrent replays can't both win β the same DB-level pattern
+// as booking's conditional slot update (ARCHITECTURE.md Β§5.1).
+//
+// Deliberately no other fields: no XDR, no signature, no PHI. The
+// challenge itself is re-derivable from the submitted XDR at verify
+// time; this table only proves "we issued this nonce to this user for
+// this account, it hasn't been used, and it hasn't expired."
+model Sep10Challenge {
+ id String @id @default(uuid())
+ userId String
+ account String // the G... (or M...) account the challenge was issued for
+ nonce String @unique // hex/utf8 nonce from the challenge's Manage Data op
+ expiresAt DateTime // matches the challenge's timebounds maxTime (+ record TTL)
+ consumedAt DateTime?
+ createdAt DateTime @default(now())
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ // Pruning and the "is this row still redeemable" check both scan by
+ // expiry β same reason LoginAttempt indexes its window. Lookups at
+ // verify time go through the unique `nonce` index.
+ @@index([expiresAt])
+}
+
+model WalletLink {
+ id String @id @default(uuid())
+ // Scoped to the facility being paid, not the patient β v1 is a
+ // custodial model (docforum-core funds escrows from its own platform
+ // Stellar identity; patients never hold or sign with a wallet here).
+ // See ADR 0004.
+ facilityId String @unique
+ facility FacilityProfile @relation(fields: [facilityId], references: [id], onDelete: Cascade)
+
+ stellarPublicKey String
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
+
+// Soroban event ingestion (issue #86). Cursors are per contract deployment;
+// persisted events are immutable observations used for reconciliation/alerts.
+enum EscrowChainEventState {
+ pending
+ matched
+ unmatched
+}
+
+model EscrowEventCursor {
+ contractId String @id
+ startLedger Int
+ cursor String?
+ lastLedger Int?
+ catchupError String?
+ updatedAt DateTime @updatedAt
+}
+
+model EscrowChainEvent {
+ id String @id @default(uuid())
+ eventId String
+ contractId String
+ ledger Int
+ txHash String?
+ escrowId BigInt?
+ eventType String
+ state EscrowChainEventState @default(pending)
+ paymentIntentId String?
+ payload Json?
+ detectedAt DateTime @default(now())
+ alertedAt DateTime?
+
+ @@unique([contractId, eventId])
+ @@index([state, detectedAt])
+ @@index([contractId, ledger])
+}
+
+// --- Audit trail (Phase 8, issue #43) -------------------------------------
+// One row per state transition on a record that matters for clinical,
+// legal or financial accountability (PRD Β§7: "who, when"). Written by the
+// audit module's record(), always inside the same transaction as the
+// change it describes β if the change rolls back, so does its event.
+//
+// APPEND-ONLY. There is no update or delete path in code, and a database
+// trigger (see the add_audit_event migration) rejects UPDATE, DELETE and
+// TRUNCATE outright. Deliberately no foreign keys: an FK's ON DELETE action
+// would have to rewrite or remove audit rows (which the trigger forbids),
+// and RESTRICT would let the audit trail block account lifecycle. Ids are
+// recorded as facts about the past.
+//
+// No PHI, ever: `metadata` is a small flat map of ids/codes validated by
+// record() β never names, contact details, DOB, intake text, or copies of
+// request/response bodies.
+model AuditEvent {
+ id String @id @default(uuid())
+ // Null only for system-initiated transitions (none exist yet).
+ actorUserId String?
+ actorRole Role?
+ // e.g. 'Appointment', 'DoctorProfile', 'PaymentIntent'.
+ entityType String
+ entityId String
+ // e.g. 'appointment.booked', 'payment_intent.released'.
+ action String
+ fromState String?
+ toState String?
+ // X-Request-Id of the HTTP request that caused it (src/middleware/request-id.ts).
+ requestId String?
+ metadata Json?
+ createdAt DateTime @default(now())
+
+ @@index([entityType, entityId, createdAt])
+ @@index([actorUserId, createdAt])
+ @@index([createdAt, id])
+}
diff --git a/backend/scripts/check-i18n-catalogues.mjs b/backend/scripts/check-i18n-catalogues.mjs
index 15b8df0e..941385d6 100644
--- a/backend/scripts/check-i18n-catalogues.mjs
+++ b/backend/scripts/check-i18n-catalogues.mjs
@@ -1,18 +1,18 @@
-import { readFile } from 'node:fs/promises';
-
-const source = JSON.parse(await readFile(new URL('../src/i18n/messages/en.json', import.meta.url), 'utf8'));
-const locales = ['fr'];
-let failed = false;
-
-for (const locale of locales) {
- const catalogue = JSON.parse(await readFile(new URL(`../src/i18n/messages/${locale}.json`, import.meta.url), 'utf8'));
- const missing = Object.keys(source).filter((key) => !(key in catalogue));
- const extra = Object.keys(catalogue).filter((key) => !(key in source));
- if (missing.length || extra.length) {
- failed = true;
- console.error(`${locale}: missing [${missing.join(', ')}], extra [${extra.join(', ')}]`);
- }
-}
-
-if (failed) process.exitCode = 1;
-else console.log('Message catalogues have matching keys.');
+import { readFile } from 'node:fs/promises';
+
+const source = JSON.parse(await readFile(new URL('../src/i18n/messages/en.json', import.meta.url), 'utf8'));
+const locales = ['fr'];
+let failed = false;
+
+for (const locale of locales) {
+ const catalogue = JSON.parse(await readFile(new URL(`../src/i18n/messages/${locale}.json`, import.meta.url), 'utf8'));
+ const missing = Object.keys(source).filter((key) => !(key in catalogue));
+ const extra = Object.keys(catalogue).filter((key) => !(key in source));
+ if (missing.length || extra.length) {
+ failed = true;
+ console.error(`${locale}: missing [${missing.join(', ')}], extra [${extra.join(', ')}]`);
+ }
+}
+
+if (failed) process.exitCode = 1;
+else console.log('Message catalogues have matching keys.');
diff --git a/backend/scripts/gen-openapi.mjs b/backend/scripts/gen-openapi.mjs
index 1f26e99e..fb306ff7 100755
--- a/backend/scripts/gen-openapi.mjs
+++ b/backend/scripts/gen-openapi.mjs
@@ -1,38 +1,38 @@
-#!/usr/bin/env node
-import { writeFileSync, mkdirSync } from 'fs';
-import { execSync } from 'child_process';
-import { join, dirname } from 'path';
-import { fileURLToPath } from 'url';
-
-const __dirname = dirname(fileURLToPath(import.meta.url));
-const root = join(__dirname, '../..');
-
-// Generate OpenAPI spec (placeholder - in production this would introspect routes)
-const openapi = {
- openapi: '3.0.3',
- info: {
- title: 'DocForum API',
- description: 'DocForum backend API - modular monolith',
- version: '1.0.0'
- },
- servers: [
- { url: 'http://localhost:4000', description: 'Local development' }
- ],
- paths: {}
-};
-
-// Write spec
-writeFileSync(join(root, 'backend/openapi.json'), JSON.stringify(openapi, null, 2));
-
-// Generate types
-mkdirSync(join(root, 'packages/api-client/src'), { recursive: true });
-try {
- execSync('npx openapi-typescript backend/openapi.json -o packages/api-client/src/types.ts', {
- cwd: root,
- stdio: 'inherit'
- });
- console.log('Types generated');
-} catch (e) {
- console.error('Failed to generate types:', e.message);
- process.exit(1);
-}
+#!/usr/bin/env node
+import { writeFileSync, mkdirSync } from 'fs';
+import { execSync } from 'child_process';
+import { join, dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const root = join(__dirname, '../..');
+
+// Generate OpenAPI spec (placeholder - in production this would introspect routes)
+const openapi = {
+ openapi: '3.0.3',
+ info: {
+ title: 'DocForum API',
+ description: 'DocForum backend API - modular monolith',
+ version: '1.0.0'
+ },
+ servers: [
+ { url: 'http://localhost:4000', description: 'Local development' }
+ ],
+ paths: {}
+};
+
+// Write spec
+writeFileSync(join(root, 'backend/openapi.json'), JSON.stringify(openapi, null, 2));
+
+// Generate types
+mkdirSync(join(root, 'packages/api-client/src'), { recursive: true });
+try {
+ execSync('npx openapi-typescript backend/openapi.json -o packages/api-client/src/types.ts', {
+ cwd: root,
+ stdio: 'inherit'
+ });
+ console.log('Types generated');
+} catch (e) {
+ console.error('Failed to generate types:', e.message);
+ process.exit(1);
+}
diff --git a/backend/scripts/gen-platform-identities.mjs b/backend/scripts/gen-platform-identities.mjs
index 5c102776..481e3597 100644
--- a/backend/scripts/gen-platform-identities.mjs
+++ b/backend/scripts/gen-platform-identities.mjs
@@ -1,61 +1,61 @@
-// Generates fresh testnet payer/releaser identities. Locally, credentials
-// are appended directly to .env; in Actions, they go to GITHUB_ENV and are
-// masked. Secret values are never printed. See ADR 0004.
-import { Keypair } from "@stellar/stellar-sdk";
-import fs from "node:fs";
-import path from "node:path";
-import { fileURLToPath } from "node:url";
-
-const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
-const ENV_PATH = path.join(ROOT, ".env");
-
-const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
-
-async function fund(publicKey) {
- const maxAttempts = 5;
- let lastFailure;
- let attempts = 0;
- for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
- attempts = attempt + 1;
- try {
- const res = await fetch(`https://friendbot.stellar.org?addr=${encodeURIComponent(publicKey)}`);
- if (res.ok) return;
- lastFailure = `HTTP ${res.status}`;
- if (res.status !== 429 && res.status < 500) break;
- } catch (error) {
- lastFailure = error instanceof Error ? error.name : "network error";
- }
- if (attempt < maxAttempts - 1) {
- const backoff = Math.min(1000 * 2 ** attempt, 16_000);
- await wait(backoff + Math.floor(Math.random() * 500));
- }
- }
- throw new Error(`Friendbot funding failed for ${publicKey} after ${attempts} attempt(s) (${lastFailure}).`);
-}
-
-const payer = Keypair.random();
-const releaser = Keypair.random();
-
-await fund(payer.publicKey());
-await fund(releaser.publicKey());
-
-const envLines = [
- `PLATFORM_PAYER_SECRET=${payer.secret()}`,
- `PLATFORM_PAYER_PUBLIC_KEY=${payer.publicKey()}`,
- `PLATFORM_RELEASER_SECRET=${releaser.secret()}`,
- `PLATFORM_RELEASER_PUBLIC_KEY=${releaser.publicKey()}`,
-].join("\n");
-
-if (process.env.GITHUB_ENV) {
- process.stdout.write(`::add-mask::${payer.secret()}\n::add-mask::${releaser.secret()}\n`);
- fs.appendFileSync(process.env.GITHUB_ENV, `${envLines}\n`);
- console.log("Funded fresh testnet identities for this workflow run.");
-} else {
- fs.appendFileSync(
- ENV_PATH,
- ["", "# Generated by scripts/gen-platform-identities.mjs β testnet only, never commit.", envLines, ""].join("\n"),
- );
- console.log("Funded and wrote secrets to .env (not printed here). Public keys:");
- console.log(" payer: ", payer.publicKey());
- console.log(" releaser:", releaser.publicKey());
-}
+// Generates fresh testnet payer/releaser identities. Locally, credentials
+// are appended directly to .env; in Actions, they go to GITHUB_ENV and are
+// masked. Secret values are never printed. See ADR 0004.
+import { Keypair } from "@stellar/stellar-sdk";
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const ENV_PATH = path.join(ROOT, ".env");
+
+const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
+
+async function fund(publicKey) {
+ const maxAttempts = 5;
+ let lastFailure;
+ let attempts = 0;
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
+ attempts = attempt + 1;
+ try {
+ const res = await fetch(`https://friendbot.stellar.org?addr=${encodeURIComponent(publicKey)}`);
+ if (res.ok) return;
+ lastFailure = `HTTP ${res.status}`;
+ if (res.status !== 429 && res.status < 500) break;
+ } catch (error) {
+ lastFailure = error instanceof Error ? error.name : "network error";
+ }
+ if (attempt < maxAttempts - 1) {
+ const backoff = Math.min(1000 * 2 ** attempt, 16_000);
+ await wait(backoff + Math.floor(Math.random() * 500));
+ }
+ }
+ throw new Error(`Friendbot funding failed for ${publicKey} after ${attempts} attempt(s) (${lastFailure}).`);
+}
+
+const payer = Keypair.random();
+const releaser = Keypair.random();
+
+await fund(payer.publicKey());
+await fund(releaser.publicKey());
+
+const envLines = [
+ `PLATFORM_PAYER_SECRET=${payer.secret()}`,
+ `PLATFORM_PAYER_PUBLIC_KEY=${payer.publicKey()}`,
+ `PLATFORM_RELEASER_SECRET=${releaser.secret()}`,
+ `PLATFORM_RELEASER_PUBLIC_KEY=${releaser.publicKey()}`,
+].join("\n");
+
+if (process.env.GITHUB_ENV) {
+ process.stdout.write(`::add-mask::${payer.secret()}\n::add-mask::${releaser.secret()}\n`);
+ fs.appendFileSync(process.env.GITHUB_ENV, `${envLines}\n`);
+ console.log("Funded fresh testnet identities for this workflow run.");
+} else {
+ fs.appendFileSync(
+ ENV_PATH,
+ ["", "# Generated by scripts/gen-platform-identities.mjs β testnet only, never commit.", envLines, ""].join("\n"),
+ );
+ console.log("Funded and wrote secrets to .env (not printed here). Public keys:");
+ console.log(" payer: ", payer.publicKey());
+ console.log(" releaser:", releaser.publicKey());
+}
diff --git a/backend/src/admin-overview-sections.ts b/backend/src/admin-overview-sections.ts
index 20a39f2b..7825a25e 100644
--- a/backend/src/admin-overview-sections.ts
+++ b/backend/src/admin-overview-sections.ts
@@ -1,28 +1,28 @@
-// Composition root for the admin overview (Phase 8, issue #123). Each module
-// owns its section in its services/; this file is the explicit list of what
-// the overview shows, so adding a queue is one line here. Lives outside
-// modules/ because it is wiring, not business logic.
-//
-// Not yet contributed: payments stuck in intermediate states, failed jobs,
-// referrals pending too long, orders not routed to a facility. Each needs a
-// count-and-age service in its own module before it can be listed here.
-import { doctorVerificationQueueCounts } from './modules/doctors/services';
-import {
- createOverviewCache,
- runOverviewSections,
- type AdminOverview,
- type AdminOverviewSectionDefinition,
-} from './utils/admin-overview';
-
-const SECTIONS: AdminOverviewSectionDefinition[] = [
- {
- key: 'doctor-verification',
- label: 'Doctors awaiting verification',
- drillDownPath: '/doctors?verificationStatus=pending',
- run: doctorVerificationQueueCounts,
- },
-];
-
-export const getAdminOverview = createOverviewCache(() => runOverviewSections(SECTIONS));
-
-export type { AdminOverview };
+// Composition root for the admin overview (Phase 8, issue #123). Each module
+// owns its section in its services/; this file is the explicit list of what
+// the overview shows, so adding a queue is one line here. Lives outside
+// modules/ because it is wiring, not business logic.
+//
+// Not yet contributed: payments stuck in intermediate states, failed jobs,
+// referrals pending too long, orders not routed to a facility. Each needs a
+// count-and-age service in its own module before it can be listed here.
+import { doctorVerificationQueueCounts } from './modules/doctors/services';
+import {
+ createOverviewCache,
+ runOverviewSections,
+ type AdminOverview,
+ type AdminOverviewSectionDefinition,
+} from './utils/admin-overview';
+
+const SECTIONS: AdminOverviewSectionDefinition[] = [
+ {
+ key: 'doctor-verification',
+ label: 'Doctors awaiting verification',
+ drillDownPath: '/doctors?verificationStatus=pending',
+ run: doctorVerificationQueueCounts,
+ },
+];
+
+export const getAdminOverview = createOverviewCache(() => runOverviewSections(SECTIONS));
+
+export type { AdminOverview };
diff --git a/backend/src/app.ts b/backend/src/app.ts
index 8784c033..6a5101c4 100644
--- a/backend/src/app.ts
+++ b/backend/src/app.ts
@@ -1,92 +1,92 @@
-import express from 'express';
-import cors from 'cors';
-import cookieParser from 'cookie-parser';
-import { config } from './config';
-import { errorHandler } from './middleware/error-handler';
-import { adminAuthRouter, authRouter } from './modules/auth/routes';
-import { doctorsRouter, specialtiesRouter } from './modules/doctors/routes';
-import { availabilityRouter } from './modules/availability/routes';
-import { appointmentsRouter } from './modules/appointments/routes';
-import { appointmentConsultationsRouter, consultationsRouter } from './modules/consultations/routes';
-import { ordersRouter } from './modules/orders/routes';
-import { adminOrdersRouter } from './modules/orders/routes/admin';
-import { pharmacyOrdersRouter } from './modules/pharmacy-orders/routes';
-import { labOrdersRouter } from './modules/lab-orders/routes';
-import { paymentsRouter } from './modules/payments/routes';
-import { patientsRouter } from './modules/patients/routes';
-import { facilitiesRouter } from './modules/facilities/routes';
-import { adminRouter } from './modules/users/routes';
-import { registerPatientExportSections } from './export-sections';
-import { auditRouter, adminAuditRouter, patientAuditRouter } from './modules/audit/routes';
-import { escrowEventsRouter } from './modules/payments/routes/escrow-events';
-import { requestId } from './middleware/request-id';
-import { referralsRouter } from './modules/referrals/routes';
-import { notificationsRouter } from './modules/notifications/routes';
-import { registerRetentionHolds } from './retention-holds';
-
-export function createApp() {
- const app = express();
-
- // Phase 8, issue #40: idempotent, so building the app twice is harmless.
- registerPatientExportSections();
- // Phase 8, issue #41: idempotent, so building the app twice is harmless.
- registerRetentionHolds();
-
- // Phase 8, issue #35: login throttling keys on the client IP, so the
- // proxy chain has to be resolved deliberately rather than guessed. See
- // config.trustProxy for why neither `true` nor the default is safe here.
- app.set('trust proxy', config.trustProxy);
-
- // Explicit allowlist per environment (Phase 8, issue #37) β see
- // config.corsOrigins. `credentials: true` is required for the refresh
- // cookie and is exactly why the allowlist can't be a wildcard.
- // Phase 8, issue #43: first, so every response (errors included) carries
- // an X-Request-Id and audit events can be tied back to it. Phase 9,
- // issue #46: the same middleware attaches a request-scoped child logger
- // (`req.log`) so every log line in a request can be greped by that id.
- app.use(requestId);
- app.use(cors({ origin: config.corsOrigins, credentials: true }));
- app.use(express.json());
- app.use(cookieParser());
-
- app.get('/health', (_req, res) => res.status(200).json({ status: 'ok' }));
-
- app.use('/auth', authRouter);
- // Admin surface. Currently only the auth module's session administration
- // (Phase 8 #38) β other modules' admin routes can hang off the same mount.
- app.use('/admin', adminAuthRouter);
- app.use('/doctors', doctorsRouter);
- app.use('/specialties', specialtiesRouter);
- app.use('/availability', availabilityRouter);
- app.use('/appointments', appointmentsRouter);
- // Closing a consultation is addressable from the appointment it belongs to
- // (POST /appointments/:appointmentId/consultation) as well as from
- // /consultations below β same service, one write path.
- app.use('/appointments', appointmentConsultationsRouter);
- app.use('/orders', ordersRouter);
- app.use('/pharmacy-orders', pharmacyOrdersRouter);
- app.use('/lab-orders', labOrdersRouter);
- app.use('/admin', adminOrdersRouter);
- app.use('/payments', paymentsRouter);
- app.use('/patients', patientsRouter);
- app.use('/facilities', facilitiesRouter);
- // Phase 8, issue #39 β admin account lifecycle (invites, deactivate/
- // reactivate) alongside the audit read path. Different paths, no overlap.
- app.use('/admin', adminRouter);
- app.use('/admin', auditRouter);
- app.use('/admin/payments', escrowEventsRouter);
-
- // PHI read-access logging (Phase 8, issue #45). Mounted at root so the
- // paths match the issue's spec exactly: /admin/access-logs, /patients/me/access-log.
- app.use('/admin', adminAuditRouter);
- app.use('/patients', patientAuditRouter);
- app.use('/referrals', referralsRouter);
- app.use('/notifications', notificationsRouter);
- app.use('/consultations', consultationsRouter);
-
- // Must be registered last β Express only routes errors here once every
- // other handler has had a chance to run.
- app.use(errorHandler);
-
- return app;
-}
+import express from 'express';
+import cors from 'cors';
+import cookieParser from 'cookie-parser';
+import { config } from './config';
+import { errorHandler } from './middleware/error-handler';
+import { adminAuthRouter, authRouter } from './modules/auth/routes';
+import { doctorsRouter, specialtiesRouter } from './modules/doctors/routes';
+import { availabilityRouter } from './modules/availability/routes';
+import { appointmentsRouter } from './modules/appointments/routes';
+import { appointmentConsultationsRouter, consultationsRouter } from './modules/consultations/routes';
+import { ordersRouter } from './modules/orders/routes';
+import { adminOrdersRouter } from './modules/orders/routes/admin';
+import { pharmacyOrdersRouter } from './modules/pharmacy-orders/routes';
+import { labOrdersRouter } from './modules/lab-orders/routes';
+import { paymentsRouter } from './modules/payments/routes';
+import { patientsRouter } from './modules/patients/routes';
+import { facilitiesRouter } from './modules/facilities/routes';
+import { adminRouter } from './modules/users/routes';
+import { registerPatientExportSections } from './export-sections';
+import { auditRouter, adminAuditRouter, patientAuditRouter } from './modules/audit/routes';
+import { escrowEventsRouter } from './modules/payments/routes/escrow-events';
+import { requestId } from './middleware/request-id';
+import { referralsRouter } from './modules/referrals/routes';
+import { notificationsRouter } from './modules/notifications/routes';
+import { registerRetentionHolds } from './retention-holds';
+
+export function createApp() {
+ const app = express();
+
+ // Phase 8, issue #40: idempotent, so building the app twice is harmless.
+ registerPatientExportSections();
+ // Phase 8, issue #41: idempotent, so building the app twice is harmless.
+ registerRetentionHolds();
+
+ // Phase 8, issue #35: login throttling keys on the client IP, so the
+ // proxy chain has to be resolved deliberately rather than guessed. See
+ // config.trustProxy for why neither `true` nor the default is safe here.
+ app.set('trust proxy', config.trustProxy);
+
+ // Explicit allowlist per environment (Phase 8, issue #37) β see
+ // config.corsOrigins. `credentials: true` is required for the refresh
+ // cookie and is exactly why the allowlist can't be a wildcard.
+ // Phase 8, issue #43: first, so every response (errors included) carries
+ // an X-Request-Id and audit events can be tied back to it. Phase 9,
+ // issue #46: the same middleware attaches a request-scoped child logger
+ // (`req.log`) so every log line in a request can be greped by that id.
+ app.use(requestId);
+ app.use(cors({ origin: config.corsOrigins, credentials: true }));
+ app.use(express.json());
+ app.use(cookieParser());
+
+ app.get('/health', (_req, res) => res.status(200).json({ status: 'ok' }));
+
+ app.use('/auth', authRouter);
+ // Admin surface. Currently only the auth module's session administration
+ // (Phase 8 #38) β other modules' admin routes can hang off the same mount.
+ app.use('/admin', adminAuthRouter);
+ app.use('/doctors', doctorsRouter);
+ app.use('/specialties', specialtiesRouter);
+ app.use('/availability', availabilityRouter);
+ app.use('/appointments', appointmentsRouter);
+ // Closing a consultation is addressable from the appointment it belongs to
+ // (POST /appointments/:appointmentId/consultation) as well as from
+ // /consultations below β same service, one write path.
+ app.use('/appointments', appointmentConsultationsRouter);
+ app.use('/orders', ordersRouter);
+ app.use('/pharmacy-orders', pharmacyOrdersRouter);
+ app.use('/lab-orders', labOrdersRouter);
+ app.use('/admin', adminOrdersRouter);
+ app.use('/payments', paymentsRouter);
+ app.use('/patients', patientsRouter);
+ app.use('/facilities', facilitiesRouter);
+ // Phase 8, issue #39 β admin account lifecycle (invites, deactivate/
+ // reactivate) alongside the audit read path. Different paths, no overlap.
+ app.use('/admin', adminRouter);
+ app.use('/admin', auditRouter);
+ app.use('/admin/payments', escrowEventsRouter);
+
+ // PHI read-access logging (Phase 8, issue #45). Mounted at root so the
+ // paths match the issue's spec exactly: /admin/access-logs, /patients/me/access-log.
+ app.use('/admin', adminAuditRouter);
+ app.use('/patients', patientAuditRouter);
+ app.use('/referrals', referralsRouter);
+ app.use('/notifications', notificationsRouter);
+ app.use('/consultations', consultationsRouter);
+
+ // Must be registered last β Express only routes errors here once every
+ // other handler has had a chance to run.
+ app.use(errorHandler);
+
+ return app;
+}
diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts
index 102952c9..8e8a9d44 100644
--- a/backend/src/config/index.ts
+++ b/backend/src/config/index.ts
@@ -1,311 +1,311 @@
-import 'dotenv/config';
-import { parsePaymentsConfig } from './payments';
-
-const payoutPrivacyMode = process.env.PAYOUT_PRIVACY_MODE ?? 'facility';
-if (payoutPrivacyMode !== 'facility' && payoutPrivacyMode !== 'pooled') {
- throw new Error('PAYOUT_PRIVACY_MODE must be either "facility" or "pooled".');
-}
-
-/** A strictly positive number from an env var, else the default. A typo like
- * `REFERRAL_PENDING_TTL_DAYS=0` must not silently expire every referral. */
-function positiveNumber(raw: string | undefined, fallback: number): number {
- const value = Number(raw);
- return raw !== undefined && raw !== '' && Number.isFinite(value) && value > 0 ? value : fallback;
-}
-
-function requireEnv(name: string): string {
- const value = process.env[name];
- if (!value) throw new Error(`Missing required env var: ${name}`);
- return value;
-}
-
-/** `TRUST_PROXY` accepts what Express accepts: `true`/`false`, a hop count
- * (`1`), or a preset string (`loopback`). Absent, it defaults to `false`
- * everywhere except production, where a proxy is assumed in front. */
-function parseTrustProxy(raw: string | undefined, isProduction: boolean): boolean | number | string {
- if (raw === undefined || raw === '') return isProduction ? 1 : false;
- if (raw === 'true') return true;
- if (raw === 'false') return false;
- if (/^\d+$/.test(raw)) return Number(raw);
- return raw;
-}
-
-/** A payment asset the escrow may be denominated in (issue #79). The
- * contract id is the Stellar Asset Contract address the escrow pulls from;
- * `decimals` is informational for clients (every amount in this API is
- * already in the asset's smallest unit, so the server never scales one). */
-export interface PaymentAsset {
- code: string;
- tokenContractId: string;
- decimals: number;
- /** Set for issued assets (e.g. USDC). Native XLM has none and needs no
- * trustline; every issued asset's payee must have one before funding. */
- issuer?: string;
-}
-
-// Testnet defaults. Override per environment with PAYMENT_ASSETS; production
-// must set it explicitly (mainnet contract ids differ). Only assets without
-// clawback, transfer fees or rebasing belong in the allowlist β escrow
-// threat model T5 β and the parser refuses entries that opt into clawback or
-// rebasing. This is a curated policy list, not an on-chain capability probe.
-const TESTNET_NATIVE_SAC = 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC';
-const TESTNET_USDC_SAC = 'CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA';
-const TESTNET_USDC_ISSUER = 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5';
-
-const STELLAR_CONTRACT_ID = /^C[A-Z2-7]{55}$/;
-const STELLAR_PUBLIC_KEY = /^G[A-Z2-7]{55}$/;
-
-function parsePaymentAssets(raw: string | undefined): PaymentAsset[] {
- const fallback: PaymentAsset[] = [
- { code: 'XLM', tokenContractId: process.env.ESCROW_TOKEN_ID ?? TESTNET_NATIVE_SAC, decimals: 7 },
- {
- code: 'USDC',
- tokenContractId: TESTNET_USDC_SAC,
- decimals: 7,
- issuer: TESTNET_USDC_ISSUER,
- },
- ];
- if (!raw) return fallback;
-
- let parsed: unknown;
- try {
- parsed = JSON.parse(raw);
- } catch {
- throw new Error('PAYMENT_ASSETS must be a JSON array of { code, tokenContractId, decimals, issuer? }.');
- }
- if (!Array.isArray(parsed) || parsed.length === 0) {
- throw new Error('PAYMENT_ASSETS must be a non-empty JSON array.');
- }
- const seen = new Set();
- return parsed.map((entry, index) => {
- if (typeof entry !== 'object' || entry === null) throw new Error(`PAYMENT_ASSETS[${index}] must be an object.`);
- const { code, tokenContractId, decimals, issuer, clawback, rebase } = entry as Record;
- if (clawback === true || rebase === true) {
- throw new Error(`PAYMENT_ASSETS[${index}] opts into clawback/rebasing, which the escrow allowlist forbids (T5).`);
- }
- if (typeof code !== 'string' || !/^[A-Z0-9]{1,12}$/.test(code)) {
- throw new Error(`PAYMENT_ASSETS[${index}].code must be 1-12 uppercase alphanumeric characters.`);
- }
- if (seen.has(code)) throw new Error(`PAYMENT_ASSETS contains duplicate asset code ${code}.`);
- seen.add(code);
- if (typeof tokenContractId !== 'string' || !STELLAR_CONTRACT_ID.test(tokenContractId)) {
- throw new Error(`PAYMENT_ASSETS[${index}].tokenContractId must be a Stellar contract id.`);
- }
- if (typeof decimals !== 'number' || !Number.isInteger(decimals) || decimals < 0 || decimals > 18) {
- throw new Error(`PAYMENT_ASSETS[${index}].decimals must be an integer between 0 and 18.`);
- }
- if (issuer !== undefined && (typeof issuer !== 'string' || !STELLAR_PUBLIC_KEY.test(issuer))) {
- throw new Error(`PAYMENT_ASSETS[${index}].issuer must be a Stellar public key.`);
- }
- return { code, tokenContractId, decimals, ...(issuer ? { issuer } : {}) };
- });
-}
-
-const PRODUCTION_CORS_ORIGINS = ['https://docforum.github.io'];
-const LOCAL_DEV_CORS_ORIGINS = ['http://localhost:5173', 'http://127.0.0.1:5173'];
-
-function parseCorsOrigins(raw: string | undefined, isProduction: boolean): string[] {
- if (!raw) return isProduction ? PRODUCTION_CORS_ORIGINS : LOCAL_DEV_CORS_ORIGINS;
- return raw
- .split(',')
- .map((origin) => origin.trim())
- .filter((origin) => origin.length > 0);
-}
-
-export const config = {
- port: Number(process.env.PORT ?? 4000),
- databaseUrl: requireEnv('DATABASE_URL'),
- jwt: {
- accessSecret: requireEnv('JWT_ACCESS_SECRET'),
- refreshSecret: requireEnv('JWT_REFRESH_SECRET'),
- // Short-lived access token; refresh token is the long-lived one.
- // No revocation list yet for either β see ARCHITECTURE_ESSENTIALS.md
- // "Known unresolved design gaps" (JWT refresh rotation without a
- // revocation list). A stolen refresh token has no clean kill switch
- // until that's built (Phase 8).
- accessTtl: '15m',
- refreshTtl: '30d',
- },
- // Base URL of docforum-web, used to build the link in outbound
- // notifications (password reset today). Not a provider decision β see
- // issue #17 β just the URL the recipient is sent to.
- appBaseUrl: process.env.APP_BASE_URL ?? 'http://localhost:5173',
- passwordReset: {
- // Raw tokens are single-use and short-lived; only their SHA-256 hash
- // is stored (see PasswordResetToken in schema.prisma).
- tokenTtlMinutes: Number(process.env.PASSWORD_RESET_TOKEN_TTL_MINUTES ?? 30),
- // Rate limits on POST /auth/password-reset/request, so the endpoint
- // can't be used to spam an inbox. Applied per email AND per IP β
- // either limit being hit returns 429. See auth/services/rate-limit.ts
- // for the (in-memory, single-process) caveat.
- requestsPerEmailPerHour: Number(process.env.PASSWORD_RESET_REQUESTS_PER_EMAIL_PER_HOUR ?? 3),
- requestsPerIpPerHour: Number(process.env.PASSWORD_RESET_REQUESTS_PER_IP_PER_HOUR ?? 10),
- },
- // Phase 8 hardening knobs (issues #34/#35/#37). Env-configurable so a
- // preview deployment can be tightened without a code change, and so
- // tests can set short windows instead of waiting real minutes.
- auth: {
- emailVerification: {
- // How long a verification link stays usable.
- tokenTtlMinutes: Number(process.env.EMAIL_VERIFICATION_TOKEN_TTL_MINUTES ?? 24 * 60),
- // Minimum gap between two "resend verification email" requests for
- // the same account (a resend is otherwise an open email-flooding
- // button).
- resendIntervalSeconds: Number(process.env.EMAIL_VERIFICATION_RESEND_INTERVAL_SECONDS ?? 60),
- // Base URL the verification link points at. docforum-web owns the
- // page that consumes `?token=`; this is just where we send people.
- landingUrl: process.env.EMAIL_VERIFICATION_LANDING_URL ?? 'http://localhost:5173/verify-email',
- },
- },
- // Express's `trust proxy` setting (Phase 8, issue #35). Login throttling
- // keys off the client IP, and a wrong setting here is a security bug in
- // both directions:
- // - too trusting (`true`) means a client can spoof `X-Forwarded-For`
- // and get a fresh throttle bucket per request;
- // - not trusting at all means every request looks like it came from
- // Render's proxy β one shared bucket for the whole internet.
- // Default: local dev has no proxy in front (false). Production is set by
- // the deployment (Render terminates TLS in front of exactly one hop), so
- // set TRUST_PROXY explicitly there; `1` is the value that matches it.
- trustProxy: parseTrustProxy(process.env.TRUST_PROXY, process.env.NODE_ENV === 'production'),
- loginThrottle: {
- // Sliding window: failures older than this don't count toward a lock.
- windowSeconds: Number(process.env.LOGIN_THROTTLE_WINDOW_SECONDS ?? 15 * 60),
- // After this many failures (still inside the window) start refusing with
- // Retry-After, growing exponentially. Deliberately below the lock
- // threshold: slow them down before locking them out.
- backoffAfterFailures: Number(process.env.LOGIN_THROTTLE_BACKOFF_AFTER ?? 3),
- backoffBaseSeconds: Number(process.env.LOGIN_THROTTLE_BACKOFF_BASE_SECONDS ?? 2),
- backoffMaxSeconds: Number(process.env.LOGIN_THROTTLE_BACKOFF_MAX_SECONDS ?? 300),
- // The actual lock, applied per normalised email (and per IP).
- lockAfterFailures: Number(process.env.LOGIN_THROTTLE_LOCK_AFTER ?? 10),
- lockSeconds: Number(process.env.LOGIN_THROTTLE_LOCK_SECONDS ?? 15 * 60),
- // Cleanup job (src/server.ts): how often to run, and how old a bucket's
- // last failure has to be before its row is dropped.
- pruneIntervalSeconds: Number(process.env.LOGIN_THROTTLE_PRUNE_INTERVAL_SECONDS ?? 60 * 60),
- pruneAfterSeconds: Number(process.env.LOGIN_THROTTLE_PRUNE_AFTER_SECONDS ?? 24 * 60 * 60),
- },
- // PHI read-access log (Phase 8, issue #45). How long AccessLog rows are
- // kept before the prune job (src/server.ts) deletes them. Reads are far
- // higher volume than AuditEvent writes, so this gets its own, shorter
- // retention instead of AuditEvent's append-forever lifetime.
- accessLog: {
- retentionDays: Number(process.env.ACCESS_LOG_RETENTION_DAYS ?? 365),
- },
- // Patient data export (Phase 8, issue #40). Env-configurable so a
- // deployment can tune retention and memory bounds without a code change.
- patientExport: {
- // How long a `ready` export stays downloadable before the cleanup job
- // deletes its artifact. Downloads are refused at expiry even if cleanup
- // hasn't run yet.
- ttlHours: Number(process.env.PATIENT_EXPORT_TTL_HOURS ?? 24),
- // Rows fetched per section page while generating β the memory bound on
- // the read side.
- pageSize: Number(process.env.PATIENT_EXPORT_PAGE_SIZE ?? 200),
- // Target size of one stored artifact chunk β the memory bound on the
- // write side and on download (one chunk is in memory at a time).
- chunkBytes: Number(process.env.PATIENT_EXPORT_CHUNK_BYTES ?? 256 * 1024),
- // Worker cadence (src/jobs/patient-exports.ts). A new request also
- // wakes the worker immediately; the interval is the restart/backlog
- // safety net.
- workerIntervalSeconds: Number(process.env.PATIENT_EXPORT_WORKER_INTERVAL_SECONDS ?? 15),
- cleanupIntervalSeconds: Number(process.env.PATIENT_EXPORT_CLEANUP_INTERVAL_SECONDS ?? 5 * 60),
- // A `processing` job whose claim is older than this is presumed dead
- // (process crashed mid-generation) and is retried, up to maxAttempts.
- staleAfterSeconds: Number(process.env.PATIENT_EXPORT_STALE_AFTER_SECONDS ?? 15 * 60),
- maxAttempts: Number(process.env.PATIENT_EXPORT_MAX_ATTEMPTS ?? 3),
- },
-
- // Account deletion purge (Phase 8, issue #41, docs/adr/0010). Only
- // tombstones with no retained records are ever purged; the grace period is
- // operational (lets requests in flight under a still-valid access token
- // finish), not a legal retention period β that one is an open decision.
- accountPurge: {
- graceHours: Number(process.env.ACCOUNT_PURGE_GRACE_HOURS ?? 24),
- intervalSeconds: Number(process.env.ACCOUNT_PURGE_INTERVAL_SECONDS ?? 6 * 60 * 60),
- },
- // Referral expiry (Phase 3, issue #8). A referral still `pending` this
- // long after it was created β or after it last returned to `pending` β
- // is moved to the terminal `expired` state by an interval sweep
- // (src/jobs/referral-expiry.ts). A v1 default, not a tuned value.
- referrals: {
- pendingTtlDays: positiveNumber(process.env.REFERRAL_PENDING_TTL_DAYS, 14),
- expirySweepIntervalSeconds: positiveNumber(process.env.REFERRAL_EXPIRY_SWEEP_INTERVAL_SECONDS, 60 * 60),
- },
- // Guardians and dependents (Phase 8, issue #42, docs/adr/0011). Guardian
- // access ends automatically when the dependent reaches this age (UTC
- // calendar date, checked at request time).
- patients: {
- ageOfMajorityYears: Number(process.env.AGE_OF_MAJORITY_YEARS ?? 18),
- },
- // Explicit, per-environment CORS allowlist (Phase 8, issue #37). A single
- // configured origin string was one typo away from "reflect whatever origin
- // asked"; an allowlist makes every permitted frontend a deliberate entry.
- // `CORS_ORIGINS` (comma-separated) is the preferred form; `CORS_ORIGIN` is
- // still honoured as a single-origin setting so the existing Render
- // configuration (docs/adr/0003) keeps working unchanged.
- corsOrigins: parseCorsOrigins(
- process.env.CORS_ORIGINS ?? process.env.CORS_ORIGIN,
- process.env.NODE_ENV === 'production',
- ),
- cookie: {
- // The refresh cookie is `SameSite=None; Secure` in production because
- // docforum-web (GitHub Pages) and this API (Render) are genuinely
- // different sites β see docs/adr/0003. `Secure` needs HTTPS, which local
- // dev over plain http doesn't have, hence the NODE_ENV branch. Shared
- // with the CSRF cookie so the two can never drift apart.
- secure: process.env.NODE_ENV === 'production',
- sameSite: process.env.NODE_ENV === 'production' ? ('none' as const) : ('lax' as const),
- path: '/',
- },
- // Admin invites (Phase 8, issue #39). `acceptUrl` is where the invite link
- // points β docforum-web owns the page that consumes `?token=`.
- adminInvites: {
- ttlHours: Number(process.env.ADMIN_INVITE_TTL_HOURS ?? 72),
- acceptUrl: process.env.ADMIN_INVITE_ACCEPT_URL ?? 'http://localhost:5173/accept-admin-invite',
- },
- orders: {
- confirmationWindowHours: Number(process.env.ORDER_CONFIRMATION_WINDOW_HOURS ?? 72),
- expiryIntervalSeconds: Number(process.env.ORDER_EXPIRY_INTERVAL_SECONDS ?? 300),
- },
- payments: {
- ...parsePaymentsConfig(process.env),
- // Issue #79: per-environment asset allowlist (USDC + native XLM by
- // default on testnet). The legacy `escrowTokenId`/`assetAllowlist`
- // fields above remain for the pre-#79 native-XLM path.
- assets: parsePaymentAssets(process.env.PAYMENT_ASSETS),
- // Horizon is read only for payee trustline checks β never for contract
- // calls, which go through the SDK.
- horizonUrl: process.env.HORIZON_URL ?? 'https://horizon-testnet.stellar.org',
- // Issue #80: how long a prepared, unsigned create_escrow transaction is
- // considered live before the expiry job returns the intent to `created`.
- prepareTtlSeconds: Number(process.env.PAYMENT_PREPARE_TTL_SECONDS ?? 5 * 60),
- // ADR 0005: refund_after = order expiry + grace. Orders are still
- // placeholders without an expiry field (Phase 4), so the intent's own
- // createdAt is the order-expiry anchor today.
- orderExpiryHours: Number(process.env.PAYMENT_ORDER_EXPIRY_HOURS ?? 72),
- refundGraceHours: Number(process.env.PAYMENT_REFUND_GRACE_HOURS ?? 24),
- // Expiry sweep cadence (src/jobs/payment-intents.ts).
- prepareExpirySweepIntervalSeconds: Number(process.env.PAYMENT_PREPARE_EXPIRY_SWEEP_INTERVAL_SECONDS ?? 60),
- payoutPrivacyMode,
- payoutPrivacyPoolAddress: process.env.PAYOUT_PRIVACY_POOL_ADDRESS,
- },
- // SEP-10 Stellar Web Authentication (issue #77, ADR 0005 step 4).
- sep10: {
- signingSecret: process.env.SEP10_SIGNING_SECRET,
- homeDomain: process.env.SEP10_HOME_DOMAIN,
- webAuthDomain: process.env.SEP10_WEB_AUTH_DOMAIN,
- networkPassphrase: process.env.SEP10_NETWORK_PASSPHRASE ?? 'Test SDF Network ; September 2015',
- horizonUrl: process.env.SEP10_HORIZON_URL ?? 'https://horizon-testnet.stellar.org',
- challengeTimeoutSeconds: Number(process.env.SEP10_CHALLENGE_TIMEOUT_SECONDS ?? 300),
- },
- // Structured logging (Phase 9). Level is configurable per environment;
- // tests run quiet by default so captured output stays readable.
- log: {
- level:
- process.env.LOG_LEVEL ??
- (process.env.NODE_ENV === 'test' ? 'silent' : process.env.NODE_ENV === 'production' ? 'info' : 'debug'),
- // Header carrying the request id; accepted from the client or generated.
- requestIdHeader: 'x-request-id',
- },
-} as const;
+import 'dotenv/config';
+import { parsePaymentsConfig } from './payments';
+
+const payoutPrivacyMode = process.env.PAYOUT_PRIVACY_MODE ?? 'facility';
+if (payoutPrivacyMode !== 'facility' && payoutPrivacyMode !== 'pooled') {
+ throw new Error('PAYOUT_PRIVACY_MODE must be either "facility" or "pooled".');
+}
+
+/** A strictly positive number from an env var, else the default. A typo like
+ * `REFERRAL_PENDING_TTL_DAYS=0` must not silently expire every referral. */
+function positiveNumber(raw: string | undefined, fallback: number): number {
+ const value = Number(raw);
+ return raw !== undefined && raw !== '' && Number.isFinite(value) && value > 0 ? value : fallback;
+}
+
+function requireEnv(name: string): string {
+ const value = process.env[name];
+ if (!value) throw new Error(`Missing required env var: ${name}`);
+ return value;
+}
+
+/** `TRUST_PROXY` accepts what Express accepts: `true`/`false`, a hop count
+ * (`1`), or a preset string (`loopback`). Absent, it defaults to `false`
+ * everywhere except production, where a proxy is assumed in front. */
+function parseTrustProxy(raw: string | undefined, isProduction: boolean): boolean | number | string {
+ if (raw === undefined || raw === '') return isProduction ? 1 : false;
+ if (raw === 'true') return true;
+ if (raw === 'false') return false;
+ if (/^\d+$/.test(raw)) return Number(raw);
+ return raw;
+}
+
+/** A payment asset the escrow may be denominated in (issue #79). The
+ * contract id is the Stellar Asset Contract address the escrow pulls from;
+ * `decimals` is informational for clients (every amount in this API is
+ * already in the asset's smallest unit, so the server never scales one). */
+export interface PaymentAsset {
+ code: string;
+ tokenContractId: string;
+ decimals: number;
+ /** Set for issued assets (e.g. USDC). Native XLM has none and needs no
+ * trustline; every issued asset's payee must have one before funding. */
+ issuer?: string;
+}
+
+// Testnet defaults. Override per environment with PAYMENT_ASSETS; production
+// must set it explicitly (mainnet contract ids differ). Only assets without
+// clawback, transfer fees or rebasing belong in the allowlist β escrow
+// threat model T5 β and the parser refuses entries that opt into clawback or
+// rebasing. This is a curated policy list, not an on-chain capability probe.
+const TESTNET_NATIVE_SAC = 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC';
+const TESTNET_USDC_SAC = 'CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA';
+const TESTNET_USDC_ISSUER = 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5';
+
+const STELLAR_CONTRACT_ID = /^C[A-Z2-7]{55}$/;
+const STELLAR_PUBLIC_KEY = /^G[A-Z2-7]{55}$/;
+
+function parsePaymentAssets(raw: string | undefined): PaymentAsset[] {
+ const fallback: PaymentAsset[] = [
+ { code: 'XLM', tokenContractId: process.env.ESCROW_TOKEN_ID ?? TESTNET_NATIVE_SAC, decimals: 7 },
+ {
+ code: 'USDC',
+ tokenContractId: TESTNET_USDC_SAC,
+ decimals: 7,
+ issuer: TESTNET_USDC_ISSUER,
+ },
+ ];
+ if (!raw) return fallback;
+
+ let parsed: unknown;
+ try {
+ parsed = JSON.parse(raw);
+ } catch {
+ throw new Error('PAYMENT_ASSETS must be a JSON array of { code, tokenContractId, decimals, issuer? }.');
+ }
+ if (!Array.isArray(parsed) || parsed.length === 0) {
+ throw new Error('PAYMENT_ASSETS must be a non-empty JSON array.');
+ }
+ const seen = new Set();
+ return parsed.map((entry, index) => {
+ if (typeof entry !== 'object' || entry === null) throw new Error(`PAYMENT_ASSETS[${index}] must be an object.`);
+ const { code, tokenContractId, decimals, issuer, clawback, rebase } = entry as Record;
+ if (clawback === true || rebase === true) {
+ throw new Error(`PAYMENT_ASSETS[${index}] opts into clawback/rebasing, which the escrow allowlist forbids (T5).`);
+ }
+ if (typeof code !== 'string' || !/^[A-Z0-9]{1,12}$/.test(code)) {
+ throw new Error(`PAYMENT_ASSETS[${index}].code must be 1-12 uppercase alphanumeric characters.`);
+ }
+ if (seen.has(code)) throw new Error(`PAYMENT_ASSETS contains duplicate asset code ${code}.`);
+ seen.add(code);
+ if (typeof tokenContractId !== 'string' || !STELLAR_CONTRACT_ID.test(tokenContractId)) {
+ throw new Error(`PAYMENT_ASSETS[${index}].tokenContractId must be a Stellar contract id.`);
+ }
+ if (typeof decimals !== 'number' || !Number.isInteger(decimals) || decimals < 0 || decimals > 18) {
+ throw new Error(`PAYMENT_ASSETS[${index}].decimals must be an integer between 0 and 18.`);
+ }
+ if (issuer !== undefined && (typeof issuer !== 'string' || !STELLAR_PUBLIC_KEY.test(issuer))) {
+ throw new Error(`PAYMENT_ASSETS[${index}].issuer must be a Stellar public key.`);
+ }
+ return { code, tokenContractId, decimals, ...(issuer ? { issuer } : {}) };
+ });
+}
+
+const PRODUCTION_CORS_ORIGINS = ['https://docforum.github.io'];
+const LOCAL_DEV_CORS_ORIGINS = ['http://localhost:5173', 'http://127.0.0.1:5173'];
+
+function parseCorsOrigins(raw: string | undefined, isProduction: boolean): string[] {
+ if (!raw) return isProduction ? PRODUCTION_CORS_ORIGINS : LOCAL_DEV_CORS_ORIGINS;
+ return raw
+ .split(',')
+ .map((origin) => origin.trim())
+ .filter((origin) => origin.length > 0);
+}
+
+export const config = {
+ port: Number(process.env.PORT ?? 4000),
+ databaseUrl: requireEnv('DATABASE_URL'),
+ jwt: {
+ accessSecret: requireEnv('JWT_ACCESS_SECRET'),
+ refreshSecret: requireEnv('JWT_REFRESH_SECRET'),
+ // Short-lived access token; refresh token is the long-lived one.
+ // No revocation list yet for either β see ARCHITECTURE_ESSENTIALS.md
+ // "Known unresolved design gaps" (JWT refresh rotation without a
+ // revocation list). A stolen refresh token has no clean kill switch
+ // until that's built (Phase 8).
+ accessTtl: '15m',
+ refreshTtl: '30d',
+ },
+ // Base URL of docforum-web, used to build the link in outbound
+ // notifications (password reset today). Not a provider decision β see
+ // issue #17 β just the URL the recipient is sent to.
+ appBaseUrl: process.env.APP_BASE_URL ?? 'http://localhost:5173',
+ passwordReset: {
+ // Raw tokens are single-use and short-lived; only their SHA-256 hash
+ // is stored (see PasswordResetToken in schema.prisma).
+ tokenTtlMinutes: Number(process.env.PASSWORD_RESET_TOKEN_TTL_MINUTES ?? 30),
+ // Rate limits on POST /auth/password-reset/request, so the endpoint
+ // can't be used to spam an inbox. Applied per email AND per IP β
+ // either limit being hit returns 429. See auth/services/rate-limit.ts
+ // for the (in-memory, single-process) caveat.
+ requestsPerEmailPerHour: Number(process.env.PASSWORD_RESET_REQUESTS_PER_EMAIL_PER_HOUR ?? 3),
+ requestsPerIpPerHour: Number(process.env.PASSWORD_RESET_REQUESTS_PER_IP_PER_HOUR ?? 10),
+ },
+ // Phase 8 hardening knobs (issues #34/#35/#37). Env-configurable so a
+ // preview deployment can be tightened without a code change, and so
+ // tests can set short windows instead of waiting real minutes.
+ auth: {
+ emailVerification: {
+ // How long a verification link stays usable.
+ tokenTtlMinutes: Number(process.env.EMAIL_VERIFICATION_TOKEN_TTL_MINUTES ?? 24 * 60),
+ // Minimum gap between two "resend verification email" requests for
+ // the same account (a resend is otherwise an open email-flooding
+ // button).
+ resendIntervalSeconds: Number(process.env.EMAIL_VERIFICATION_RESEND_INTERVAL_SECONDS ?? 60),
+ // Base URL the verification link points at. docforum-web owns the
+ // page that consumes `?token=`; this is just where we send people.
+ landingUrl: process.env.EMAIL_VERIFICATION_LANDING_URL ?? 'http://localhost:5173/verify-email',
+ },
+ },
+ // Express's `trust proxy` setting (Phase 8, issue #35). Login throttling
+ // keys off the client IP, and a wrong setting here is a security bug in
+ // both directions:
+ // - too trusting (`true`) means a client can spoof `X-Forwarded-For`
+ // and get a fresh throttle bucket per request;
+ // - not trusting at all means every request looks like it came from
+ // Render's proxy β one shared bucket for the whole internet.
+ // Default: local dev has no proxy in front (false). Production is set by
+ // the deployment (Render terminates TLS in front of exactly one hop), so
+ // set TRUST_PROXY explicitly there; `1` is the value that matches it.
+ trustProxy: parseTrustProxy(process.env.TRUST_PROXY, process.env.NODE_ENV === 'production'),
+ loginThrottle: {
+ // Sliding window: failures older than this don't count toward a lock.
+ windowSeconds: Number(process.env.LOGIN_THROTTLE_WINDOW_SECONDS ?? 15 * 60),
+ // After this many failures (still inside the window) start refusing with
+ // Retry-After, growing exponentially. Deliberately below the lock
+ // threshold: slow them down before locking them out.
+ backoffAfterFailures: Number(process.env.LOGIN_THROTTLE_BACKOFF_AFTER ?? 3),
+ backoffBaseSeconds: Number(process.env.LOGIN_THROTTLE_BACKOFF_BASE_SECONDS ?? 2),
+ backoffMaxSeconds: Number(process.env.LOGIN_THROTTLE_BACKOFF_MAX_SECONDS ?? 300),
+ // The actual lock, applied per normalised email (and per IP).
+ lockAfterFailures: Number(process.env.LOGIN_THROTTLE_LOCK_AFTER ?? 10),
+ lockSeconds: Number(process.env.LOGIN_THROTTLE_LOCK_SECONDS ?? 15 * 60),
+ // Cleanup job (src/server.ts): how often to run, and how old a bucket's
+ // last failure has to be before its row is dropped.
+ pruneIntervalSeconds: Number(process.env.LOGIN_THROTTLE_PRUNE_INTERVAL_SECONDS ?? 60 * 60),
+ pruneAfterSeconds: Number(process.env.LOGIN_THROTTLE_PRUNE_AFTER_SECONDS ?? 24 * 60 * 60),
+ },
+ // PHI read-access log (Phase 8, issue #45). How long AccessLog rows are
+ // kept before the prune job (src/server.ts) deletes them. Reads are far
+ // higher volume than AuditEvent writes, so this gets its own, shorter
+ // retention instead of AuditEvent's append-forever lifetime.
+ accessLog: {
+ retentionDays: Number(process.env.ACCESS_LOG_RETENTION_DAYS ?? 365),
+ },
+ // Patient data export (Phase 8, issue #40). Env-configurable so a
+ // deployment can tune retention and memory bounds without a code change.
+ patientExport: {
+ // How long a `ready` export stays downloadable before the cleanup job
+ // deletes its artifact. Downloads are refused at expiry even if cleanup
+ // hasn't run yet.
+ ttlHours: Number(process.env.PATIENT_EXPORT_TTL_HOURS ?? 24),
+ // Rows fetched per section page while generating β the memory bound on
+ // the read side.
+ pageSize: Number(process.env.PATIENT_EXPORT_PAGE_SIZE ?? 200),
+ // Target size of one stored artifact chunk β the memory bound on the
+ // write side and on download (one chunk is in memory at a time).
+ chunkBytes: Number(process.env.PATIENT_EXPORT_CHUNK_BYTES ?? 256 * 1024),
+ // Worker cadence (src/jobs/patient-exports.ts). A new request also
+ // wakes the worker immediately; the interval is the restart/backlog
+ // safety net.
+ workerIntervalSeconds: Number(process.env.PATIENT_EXPORT_WORKER_INTERVAL_SECONDS ?? 15),
+ cleanupIntervalSeconds: Number(process.env.PATIENT_EXPORT_CLEANUP_INTERVAL_SECONDS ?? 5 * 60),
+ // A `processing` job whose claim is older than this is presumed dead
+ // (process crashed mid-generation) and is retried, up to maxAttempts.
+ staleAfterSeconds: Number(process.env.PATIENT_EXPORT_STALE_AFTER_SECONDS ?? 15 * 60),
+ maxAttempts: Number(process.env.PATIENT_EXPORT_MAX_ATTEMPTS ?? 3),
+ },
+
+ // Account deletion purge (Phase 8, issue #41, docs/adr/0010). Only
+ // tombstones with no retained records are ever purged; the grace period is
+ // operational (lets requests in flight under a still-valid access token
+ // finish), not a legal retention period β that one is an open decision.
+ accountPurge: {
+ graceHours: Number(process.env.ACCOUNT_PURGE_GRACE_HOURS ?? 24),
+ intervalSeconds: Number(process.env.ACCOUNT_PURGE_INTERVAL_SECONDS ?? 6 * 60 * 60),
+ },
+ // Referral expiry (Phase 3, issue #8). A referral still `pending` this
+ // long after it was created β or after it last returned to `pending` β
+ // is moved to the terminal `expired` state by an interval sweep
+ // (src/jobs/referral-expiry.ts). A v1 default, not a tuned value.
+ referrals: {
+ pendingTtlDays: positiveNumber(process.env.REFERRAL_PENDING_TTL_DAYS, 14),
+ expirySweepIntervalSeconds: positiveNumber(process.env.REFERRAL_EXPIRY_SWEEP_INTERVAL_SECONDS, 60 * 60),
+ },
+ // Guardians and dependents (Phase 8, issue #42, docs/adr/0011). Guardian
+ // access ends automatically when the dependent reaches this age (UTC
+ // calendar date, checked at request time).
+ patients: {
+ ageOfMajorityYears: Number(process.env.AGE_OF_MAJORITY_YEARS ?? 18),
+ },
+ // Explicit, per-environment CORS allowlist (Phase 8, issue #37). A single
+ // configured origin string was one typo away from "reflect whatever origin
+ // asked"; an allowlist makes every permitted frontend a deliberate entry.
+ // `CORS_ORIGINS` (comma-separated) is the preferred form; `CORS_ORIGIN` is
+ // still honoured as a single-origin setting so the existing Render
+ // configuration (docs/adr/0003) keeps working unchanged.
+ corsOrigins: parseCorsOrigins(
+ process.env.CORS_ORIGINS ?? process.env.CORS_ORIGIN,
+ process.env.NODE_ENV === 'production',
+ ),
+ cookie: {
+ // The refresh cookie is `SameSite=None; Secure` in production because
+ // docforum-web (GitHub Pages) and this API (Render) are genuinely
+ // different sites β see docs/adr/0003. `Secure` needs HTTPS, which local
+ // dev over plain http doesn't have, hence the NODE_ENV branch. Shared
+ // with the CSRF cookie so the two can never drift apart.
+ secure: process.env.NODE_ENV === 'production',
+ sameSite: process.env.NODE_ENV === 'production' ? ('none' as const) : ('lax' as const),
+ path: '/',
+ },
+ // Admin invites (Phase 8, issue #39). `acceptUrl` is where the invite link
+ // points β docforum-web owns the page that consumes `?token=`.
+ adminInvites: {
+ ttlHours: Number(process.env.ADMIN_INVITE_TTL_HOURS ?? 72),
+ acceptUrl: process.env.ADMIN_INVITE_ACCEPT_URL ?? 'http://localhost:5173/accept-admin-invite',
+ },
+ orders: {
+ confirmationWindowHours: Number(process.env.ORDER_CONFIRMATION_WINDOW_HOURS ?? 72),
+ expiryIntervalSeconds: Number(process.env.ORDER_EXPIRY_INTERVAL_SECONDS ?? 300),
+ },
+ payments: {
+ ...parsePaymentsConfig(process.env),
+ // Issue #79: per-environment asset allowlist (USDC + native XLM by
+ // default on testnet). The legacy `escrowTokenId`/`assetAllowlist`
+ // fields above remain for the pre-#79 native-XLM path.
+ assets: parsePaymentAssets(process.env.PAYMENT_ASSETS),
+ // Horizon is read only for payee trustline checks β never for contract
+ // calls, which go through the SDK.
+ horizonUrl: process.env.HORIZON_URL ?? 'https://horizon-testnet.stellar.org',
+ // Issue #80: how long a prepared, unsigned create_escrow transaction is
+ // considered live before the expiry job returns the intent to `created`.
+ prepareTtlSeconds: Number(process.env.PAYMENT_PREPARE_TTL_SECONDS ?? 5 * 60),
+ // ADR 0005: refund_after = order expiry + grace. Orders are still
+ // placeholders without an expiry field (Phase 4), so the intent's own
+ // createdAt is the order-expiry anchor today.
+ orderExpiryHours: Number(process.env.PAYMENT_ORDER_EXPIRY_HOURS ?? 72),
+ refundGraceHours: Number(process.env.PAYMENT_REFUND_GRACE_HOURS ?? 24),
+ // Expiry sweep cadence (src/jobs/payment-intents.ts).
+ prepareExpirySweepIntervalSeconds: Number(process.env.PAYMENT_PREPARE_EXPIRY_SWEEP_INTERVAL_SECONDS ?? 60),
+ payoutPrivacyMode,
+ payoutPrivacyPoolAddress: process.env.PAYOUT_PRIVACY_POOL_ADDRESS,
+ },
+ // SEP-10 Stellar Web Authentication (issue #77, ADR 0005 step 4).
+ sep10: {
+ signingSecret: process.env.SEP10_SIGNING_SECRET,
+ homeDomain: process.env.SEP10_HOME_DOMAIN,
+ webAuthDomain: process.env.SEP10_WEB_AUTH_DOMAIN,
+ networkPassphrase: process.env.SEP10_NETWORK_PASSPHRASE ?? 'Test SDF Network ; September 2015',
+ horizonUrl: process.env.SEP10_HORIZON_URL ?? 'https://horizon-testnet.stellar.org',
+ challengeTimeoutSeconds: Number(process.env.SEP10_CHALLENGE_TIMEOUT_SECONDS ?? 300),
+ },
+ // Structured logging (Phase 9). Level is configurable per environment;
+ // tests run quiet by default so captured output stays readable.
+ log: {
+ level:
+ process.env.LOG_LEVEL ??
+ (process.env.NODE_ENV === 'test' ? 'silent' : process.env.NODE_ENV === 'production' ? 'info' : 'debug'),
+ // Header carrying the request id; accepted from the client or generated.
+ requestIdHeader: 'x-request-id',
+ },
+} as const;
diff --git a/backend/src/db/prisma-client.ts b/backend/src/db/prisma-client.ts
index 8983a295..72eda762 100644
--- a/backend/src/db/prisma-client.ts
+++ b/backend/src/db/prisma-client.ts
@@ -1,32 +1,32 @@
-import { PrismaClient } from '@prisma/client';
-import { PrismaPg } from '@prisma/adapter-pg';
-import { config } from '../config';
-
-// Phase 8, issue #47: Prisma 7 dropped the Rust query engine β every client
-// is now built on a driver adapter, and `new PrismaClient()` without one
-// throws ("A driver adapter is required to connect to your database").
-// PrismaPg wraps node-postgres, so the connection still comes from
-// DATABASE_URL, exactly as it did through the v6 engine.
-//
-// Two settings below are deliberate, and both are about *behaviour parity*
-// with Prisma 6 rather than new choices (see docs/adr/0006):
-// - connectionTimeoutMillis: node-pg defaults to no connect timeout (0),
-// where v6 waited 5s. Without this, a request against a wedged database
-// hangs indefinitely instead of failing fast after 5s.
-// - `ssl` is deliberately NOT passed. Render decides whether a given
-// DATABASE_URL uses TLS and expresses that in the URL itself; forcing an
-// `ssl` object here would break the internal (non-TLS) Render URL. The
-// v6βv7 certificate-validation change is a real deployment risk β if a
-// deploy logs `P1010: User was denied access on the database`, that is
-// it, and the documented remedy is `ssl: { rejectUnauthorized: false }`
-// (docs/adr/0003, docs/adr/0006).
-const adapter = new PrismaPg({
- connectionString: config.databaseUrl,
- connectionTimeoutMillis: Number(process.env.DB_CONNECTION_TIMEOUT_MS ?? 5_000),
- max: Number(process.env.DB_POOL_MAX ?? 5),
- statement_timeout: Number(process.env.DB_STATEMENT_TIMEOUT_MS ?? 10_000),
-});
-
-// Single shared client for the whole process β Prisma's own guidance for
-// a long-running server (as opposed to a new client per request/lambda).
-export const prisma = new PrismaClient({ adapter });
+import { PrismaClient } from '@prisma/client';
+import { PrismaPg } from '@prisma/adapter-pg';
+import { config } from '../config';
+
+// Phase 8, issue #47: Prisma 7 dropped the Rust query engine β every client
+// is now built on a driver adapter, and `new PrismaClient()` without one
+// throws ("A driver adapter is required to connect to your database").
+// PrismaPg wraps node-postgres, so the connection still comes from
+// DATABASE_URL, exactly as it did through the v6 engine.
+//
+// Two settings below are deliberate, and both are about *behaviour parity*
+// with Prisma 6 rather than new choices (see docs/adr/0006):
+// - connectionTimeoutMillis: node-pg defaults to no connect timeout (0),
+// where v6 waited 5s. Without this, a request against a wedged database
+// hangs indefinitely instead of failing fast after 5s.
+// - `ssl` is deliberately NOT passed. Render decides whether a given
+// DATABASE_URL uses TLS and expresses that in the URL itself; forcing an
+// `ssl` object here would break the internal (non-TLS) Render URL. The
+// v6βv7 certificate-validation change is a real deployment risk β if a
+// deploy logs `P1010: User was denied access on the database`, that is
+// it, and the documented remedy is `ssl: { rejectUnauthorized: false }`
+// (docs/adr/0003, docs/adr/0006).
+const adapter = new PrismaPg({
+ connectionString: config.databaseUrl,
+ connectionTimeoutMillis: Number(process.env.DB_CONNECTION_TIMEOUT_MS ?? 5_000),
+ max: Number(process.env.DB_POOL_MAX ?? 5),
+ statement_timeout: Number(process.env.DB_STATEMENT_TIMEOUT_MS ?? 10_000),
+});
+
+// Single shared client for the whole process β Prisma's own guidance for
+// a long-running server (as opposed to a new client per request/lambda).
+export const prisma = new PrismaClient({ adapter });
diff --git a/backend/src/facility-hmac.ts b/backend/src/facility-hmac.ts
index 06e424e9..374e0c95 100644
--- a/backend/src/facility-hmac.ts
+++ b/backend/src/facility-hmac.ts
@@ -1,3 +1,3 @@
-import { createHmac, timingSafeEqual } from "node:crypto";
-export function signFacilityRequest(secret: string, timestamp: string, body: string): string { return createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex"); }
-export function verifyFacilityRequest(secret: string, timestamp: string, body: string, signature: string, now = Date.now()): boolean { const age=Math.abs(now-Number(timestamp)); if(!Number.isFinite(age)||age>300000||!/^[0-9]+$/.test(timestamp)) return false; const expected=Buffer.from(signFacilityRequest(secret,timestamp,body)); const actual=Buffer.from(signature); return expected.length===actual.length&&timingSafeEqual(expected,actual); }
+import { createHmac, timingSafeEqual } from "node:crypto";
+export function signFacilityRequest(secret: string, timestamp: string, body: string): string { return createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex"); }
+export function verifyFacilityRequest(secret: string, timestamp: string, body: string, signature: string, now = Date.now()): boolean { const age=Math.abs(now-Number(timestamp)); if(!Number.isFinite(age)||age>300000||!/^[0-9]+$/.test(timestamp)) return false; const expected=Buffer.from(signFacilityRequest(secret,timestamp,body)); const actual=Buffer.from(signature); return expected.length===actual.length&&timingSafeEqual(expected,actual); }
diff --git a/backend/src/i18n/index.ts b/backend/src/i18n/index.ts
index c44ff932..af81cd33 100644
--- a/backend/src/i18n/index.ts
+++ b/backend/src/i18n/index.ts
@@ -1,41 +1,41 @@
-import { IntlMessageFormat } from 'intl-messageformat';
-import en from './messages/en.json';
-import fr from './messages/fr.json';
-
-export type Locale = 'en' | 'fr';
-const catalogs: Record> = { en, fr };
-
-function asSupportedLocale(value: string | undefined): Locale | null {
- if (!value) return null;
- const language = value.trim().toLowerCase().split(/[-_]/, 1)[0];
- return language === 'en' || language === 'fr' ? language : null;
-}
-
-/** Resolve a stored preference first, then the best supported Accept-Language
- * entry. Unsupported languages fall back to the English source catalogue. */
-export function resolveLocale(preferred?: string | null, acceptLanguage?: string | string[]): Locale {
- const saved = asSupportedLocale(preferred ?? undefined);
- if (saved) return saved;
- const header = Array.isArray(acceptLanguage) ? acceptLanguage.join(',') : acceptLanguage;
- const candidates = (header ?? '')
- .split(',')
- .map((part, index) => {
- const [tag, ...params] = part.trim().split(';');
- const quality = params.map((param) => /^q\s*=\s*(\d+(?:\.\d+)?)$/i.exec(param.trim()))
- .find(Boolean)?.[1];
- return { locale: asSupportedLocale(tag), quality: quality === undefined ? 1 : Number(quality), index };
- })
- .filter((item): item is { locale: Locale; quality: number; index: number } => item.locale !== null && item.quality > 0)
- .sort((a, b) => b.quality - a.quality || a.index - b.index);
- return candidates[0]?.locale ?? 'en';
-}
-
-export function formatMessage(locale: Locale, key: string, values: Record = {}): string {
- const message = catalogs[locale][key] ?? catalogs.en[key];
- if (!message) return key;
- return String(new IntlMessageFormat(message, locale).format(values));
-}
-
-export function localizedErrorMessage(locale: Locale, code: string | undefined, fallback: string): string {
- return code && catalogs.en[`errors.${code}`] ? formatMessage(locale, `errors.${code}`) : fallback;
-}
+import { IntlMessageFormat } from 'intl-messageformat';
+import en from './messages/en.json';
+import fr from './messages/fr.json';
+
+export type Locale = 'en' | 'fr';
+const catalogs: Record> = { en, fr };
+
+function asSupportedLocale(value: string | undefined): Locale | null {
+ if (!value) return null;
+ const language = value.trim().toLowerCase().split(/[-_]/, 1)[0];
+ return language === 'en' || language === 'fr' ? language : null;
+}
+
+/** Resolve a stored preference first, then the best supported Accept-Language
+ * entry. Unsupported languages fall back to the English source catalogue. */
+export function resolveLocale(preferred?: string | null, acceptLanguage?: string | string[]): Locale {
+ const saved = asSupportedLocale(preferred ?? undefined);
+ if (saved) return saved;
+ const header = Array.isArray(acceptLanguage) ? acceptLanguage.join(',') : acceptLanguage;
+ const candidates = (header ?? '')
+ .split(',')
+ .map((part, index) => {
+ const [tag, ...params] = part.trim().split(';');
+ const quality = params.map((param) => /^q\s*=\s*(\d+(?:\.\d+)?)$/i.exec(param.trim()))
+ .find(Boolean)?.[1];
+ return { locale: asSupportedLocale(tag), quality: quality === undefined ? 1 : Number(quality), index };
+ })
+ .filter((item): item is { locale: Locale; quality: number; index: number } => item.locale !== null && item.quality > 0)
+ .sort((a, b) => b.quality - a.quality || a.index - b.index);
+ return candidates[0]?.locale ?? 'en';
+}
+
+export function formatMessage(locale: Locale, key: string, values: Record = {}): string {
+ const message = catalogs[locale][key] ?? catalogs.en[key];
+ if (!message) return key;
+ return String(new IntlMessageFormat(message, locale).format(values));
+}
+
+export function localizedErrorMessage(locale: Locale, code: string | undefined, fallback: string): string {
+ return code && catalogs.en[`errors.${code}`] ? formatMessage(locale, `errors.${code}`) : fallback;
+}
diff --git a/backend/src/i18n/messages/en.json b/backend/src/i18n/messages/en.json
index 15112439..0b70ef50 100644
--- a/backend/src/i18n/messages/en.json
+++ b/backend/src/i18n/messages/en.json
@@ -1,7 +1,7 @@
-{
- "errors.EMAIL_NOT_VERIFIED": "Verify your email address before continuing.",
- "notifications.emailVerification.subject": "Verify your DocForum email address",
- "notifications.emailVerification.body": "Confirm this email address to book appointments and pay for orders. Open this single-use link within {minutes, plural, one {# minute} other {# minutes}}: {link}",
- "notifications.passwordReset.subject": "Reset your DocForum password",
- "notifications.passwordReset.body": "A password reset was requested for this account. Open this single-use link within {minutes, plural, one {# minute} other {# minutes}} to choose a new password: {link} If you did not request this, you can ignore this message β your password is unchanged."
-}
+{
+ "errors.EMAIL_NOT_VERIFIED": "Verify your email address before continuing.",
+ "notifications.emailVerification.subject": "Verify your DocForum email address",
+ "notifications.emailVerification.body": "Confirm this email address to book appointments and pay for orders. Open this single-use link within {minutes, plural, one {# minute} other {# minutes}}: {link}",
+ "notifications.passwordReset.subject": "Reset your DocForum password",
+ "notifications.passwordReset.body": "A password reset was requested for this account. Open this single-use link within {minutes, plural, one {# minute} other {# minutes}} to choose a new password: {link} If you did not request this, you can ignore this message β your password is unchanged."
+}
diff --git a/backend/src/i18n/messages/fr.json b/backend/src/i18n/messages/fr.json
index 3939c0f3..0d104882 100644
--- a/backend/src/i18n/messages/fr.json
+++ b/backend/src/i18n/messages/fr.json
@@ -1,7 +1,7 @@
-{
- "errors.EMAIL_NOT_VERIFIED": "VΓ©rifiez votre adresse e-mail avant de continuer.",
- "notifications.emailVerification.subject": "VΓ©rifiez votre adresse e-mail DocForum",
- "notifications.emailVerification.body": "Confirmez cette adresse e-mail pour rΓ©server des rendez-vous et payer des commandes. Ouvrez ce lien Γ usage unique dans les {minutes, plural, one {# minute} other {# minutes}} : {link}",
- "notifications.passwordReset.subject": "RΓ©initialisez votre mot de passe DocForum",
- "notifications.passwordReset.body": "Une demande de rΓ©initialisation du mot de passe a Γ©tΓ© faite pour ce compte. Ouvrez ce lien Γ usage unique dans les {minutes, plural, one {# minute} other {# minutes}} pour choisir un nouveau mot de passe : {link} Si vous n'Γͺtes pas Γ l'origine de cette demande, ignorez ce message β votre mot de passe reste inchangΓ©."
-}
+{
+ "errors.EMAIL_NOT_VERIFIED": "VΓ©rifiez votre adresse e-mail avant de continuer.",
+ "notifications.emailVerification.subject": "VΓ©rifiez votre adresse e-mail DocForum",
+ "notifications.emailVerification.body": "Confirmez cette adresse e-mail pour rΓ©server des rendez-vous et payer des commandes. Ouvrez ce lien Γ usage unique dans les {minutes, plural, one {# minute} other {# minutes}} : {link}",
+ "notifications.passwordReset.subject": "RΓ©initialisez votre mot de passe DocForum",
+ "notifications.passwordReset.body": "Une demande de rΓ©initialisation du mot de passe a Γ©tΓ© faite pour ce compte. Ouvrez ce lien Γ usage unique dans les {minutes, plural, one {# minute} other {# minutes}} pour choisir un nouveau mot de passe : {link} Si vous n'Γͺtes pas Γ l'origine de cette demande, ignorez ce message β votre mot de passe reste inchangΓ©."
+}
diff --git a/backend/src/jobs/README.md b/backend/src/jobs/README.md
index b3226a70..d05e12e4 100644
--- a/backend/src/jobs/README.md
+++ b/backend/src/jobs/README.md
@@ -1,25 +1,25 @@
-# jobs
-
-Background jobs (reminders, order-expiry checks, notification delivery).
-
-Per ARCHITECTURE_ESSENTIALS.md: keep this simple (cron/interval) until a
-real job exists β do not introduce a queue (e.g. BullMQ/Redis) preemptively.
-See ARCHITECTURE.md Β§6.3.2 for the overengineering flag on this exact point.
-
-Jobs are started from `src/server.ts` only, never from `app.ts`, so test
-suites that build the app don't start timers. Each job is a thin scheduler
-around a module's service function; the state transitions themselves are
-conditional UPDATEs in that module, so running more than one process is
-safe.
-
-| Job | File | What it does |
-|---|---|---|
-| Login-throttle prune (issue #35) | `src/server.ts` | Deletes stale `LoginAttempt` buckets. |
-| Patient-export worker (issue #40) | `patient-exports.ts` | Generates pending exports one at a time; woken immediately by a new request, plus every `PATIENT_EXPORT_WORKER_INTERVAL_SECONDS`. Requeues claims older than `PATIENT_EXPORT_STALE_AFTER_SECONDS` (up to `PATIENT_EXPORT_MAX_ATTEMPTS`). |
-| Patient-export cleanup (issue #40) | `patient-exports.ts` | Every `PATIENT_EXPORT_CLEANUP_INTERVAL_SECONDS`: expires `ready` exports past `expiresAt` and deletes their chunks, plus any chunk that can never be served. |
-| Deleted-account purge (issue #41, ADR 0010) | `account-purge.ts` | Every `ACCOUNT_PURGE_INTERVAL_SECONDS`: hard-deletes anonymised patients that no module holds records for (`src/retention-holds.ts`), once `ACCOUNT_PURGE_GRACE_HOURS` has passed. Never deletes clinical, order or payment rows. Logs counts and error types only. |
-| Prepared-payment expiry (issue #80) | `payment-intents.ts` | Every `PAYMENT_PREPARE_EXPIRY_SWEEP_INTERVAL_SECONDS` (default 60): moves `PaymentIntent`s stuck in `awaiting_signature` past their `preparedExpiresAt` back to `created`, clearing the prepared transaction. Expiry is a normal case β the patient prepares again. |
-| Referral expiry (issue #8) | `referral-expiry.ts` | Every `REFERRAL_EXPIRY_SWEEP_INTERVAL_SECONDS` (default 3600), and once at startup: moves `pending` referrals untouched for `REFERRAL_PENDING_TTL_DAYS` (default 14) to `expired` with `expiredReason = 'ttl'`. `accepted` referrals are never swept. One conditional UPDATE, so multiple processes are safe. |
-
-Job logs carry error *types* only, never messages β a driver error can echo
-the patient data it was handling.
+# jobs
+
+Background jobs (reminders, order-expiry checks, notification delivery).
+
+Per ARCHITECTURE_ESSENTIALS.md: keep this simple (cron/interval) until a
+real job exists β do not introduce a queue (e.g. BullMQ/Redis) preemptively.
+See ARCHITECTURE.md Β§6.3.2 for the overengineering flag on this exact point.
+
+Jobs are started from `src/server.ts` only, never from `app.ts`, so test
+suites that build the app don't start timers. Each job is a thin scheduler
+around a module's service function; the state transitions themselves are
+conditional UPDATEs in that module, so running more than one process is
+safe.
+
+| Job | File | What it does |
+|---|---|---|
+| Login-throttle prune (issue #35) | `src/server.ts` | Deletes stale `LoginAttempt` buckets. |
+| Patient-export worker (issue #40) | `patient-exports.ts` | Generates pending exports one at a time; woken immediately by a new request, plus every `PATIENT_EXPORT_WORKER_INTERVAL_SECONDS`. Requeues claims older than `PATIENT_EXPORT_STALE_AFTER_SECONDS` (up to `PATIENT_EXPORT_MAX_ATTEMPTS`). |
+| Patient-export cleanup (issue #40) | `patient-exports.ts` | Every `PATIENT_EXPORT_CLEANUP_INTERVAL_SECONDS`: expires `ready` exports past `expiresAt` and deletes their chunks, plus any chunk that can never be served. |
+| Deleted-account purge (issue #41, ADR 0010) | `account-purge.ts` | Every `ACCOUNT_PURGE_INTERVAL_SECONDS`: hard-deletes anonymised patients that no module holds records for (`src/retention-holds.ts`), once `ACCOUNT_PURGE_GRACE_HOURS` has passed. Never deletes clinical, order or payment rows. Logs counts and error types only. |
+| Prepared-payment expiry (issue #80) | `payment-intents.ts` | Every `PAYMENT_PREPARE_EXPIRY_SWEEP_INTERVAL_SECONDS` (default 60): moves `PaymentIntent`s stuck in `awaiting_signature` past their `preparedExpiresAt` back to `created`, clearing the prepared transaction. Expiry is a normal case β the patient prepares again. |
+| Referral expiry (issue #8) | `referral-expiry.ts` | Every `REFERRAL_EXPIRY_SWEEP_INTERVAL_SECONDS` (default 3600), and once at startup: moves `pending` referrals untouched for `REFERRAL_PENDING_TTL_DAYS` (default 14) to `expired` with `expiredReason = 'ttl'`. `accepted` referrals are never swept. One conditional UPDATE, so multiple processes are safe. |
+
+Job logs carry error *types* only, never messages β a driver error can echo
+the patient data it was handling.
diff --git a/backend/src/jobs/account-purge.ts b/backend/src/jobs/account-purge.ts
index 8847d07b..47a15ae7 100644
--- a/backend/src/jobs/account-purge.ts
+++ b/backend/src/jobs/account-purge.ts
@@ -1,33 +1,33 @@
-// Deleted-account tombstone purge (Phase 8, issue #41, docs/adr/0010). A
-// plain interval, per ARCHITECTURE_ESSENTIALS.md ("keep jobs simple until a
-// real job exists β don't pre-install a queue"); move it onto a shared job
-// runner if one is ever introduced. Started from src/server.ts only, so test
-// suites that build the app never start a timer.
-import { config } from '../config';
-import { purgeDeletedPatientAccounts } from '../modules/patients/services';
-
-export function startAccountPurgeJob(): () => void {
- let running = false;
- const run = () => {
- if (running) return;
- running = true;
- void purgeDeletedPatientAccounts()
- .then(({ purged, held, failed }) => {
- if (purged > 0 || failed > 0) {
- // Counts only β never ids or personal fields.
- // eslint-disable-next-line no-console
- console.log(`[jobs] account purge: ${purged} purged, ${held} retained (records), ${failed} failed`);
- }
- })
- .catch((err) => {
- // eslint-disable-next-line no-console
- console.error(`[jobs] account purge failed: ${err instanceof Error ? err.name : typeof err}`);
- })
- .finally(() => {
- running = false;
- });
- };
- const timer = setInterval(run, config.accountPurge.intervalSeconds * 1000);
- timer.unref();
- return () => clearInterval(timer);
-}
+// Deleted-account tombstone purge (Phase 8, issue #41, docs/adr/0010). A
+// plain interval, per ARCHITECTURE_ESSENTIALS.md ("keep jobs simple until a
+// real job exists β don't pre-install a queue"); move it onto a shared job
+// runner if one is ever introduced. Started from src/server.ts only, so test
+// suites that build the app never start a timer.
+import { config } from '../config';
+import { purgeDeletedPatientAccounts } from '../modules/patients/services';
+
+export function startAccountPurgeJob(): () => void {
+ let running = false;
+ const run = () => {
+ if (running) return;
+ running = true;
+ void purgeDeletedPatientAccounts()
+ .then(({ purged, held, failed }) => {
+ if (purged > 0 || failed > 0) {
+ // Counts only β never ids or personal fields.
+ // eslint-disable-next-line no-console
+ console.log(`[jobs] account purge: ${purged} purged, ${held} retained (records), ${failed} failed`);
+ }
+ })
+ .catch((err) => {
+ // eslint-disable-next-line no-console
+ console.error(`[jobs] account purge failed: ${err instanceof Error ? err.name : typeof err}`);
+ })
+ .finally(() => {
+ running = false;
+ });
+ };
+ const timer = setInterval(run, config.accountPurge.intervalSeconds * 1000);
+ timer.unref();
+ return () => clearInterval(timer);
+}
diff --git a/backend/src/jobs/order-expiry.ts b/backend/src/jobs/order-expiry.ts
index 004bdedf..9fecd91f 100644
--- a/backend/src/jobs/order-expiry.ts
+++ b/backend/src/jobs/order-expiry.ts
@@ -1,89 +1,89 @@
-import { prisma } from '../db/prisma-client';
-import { config } from '../config';
-import { record, SYSTEM_ACTOR } from '../modules/audit/services';
-
-export async function expireUnconfirmedOrders(
- windowHours: number = config.orders.confirmationWindowHours,
-): Promise<{ confirmedCount: number }> {
- const threshold = new Date(Date.now() - windowHours * 60 * 60 * 1000);
-
- const eligibleOrders = await prisma.order.findMany({
- where: {
- status: 'fulfilled',
- confirmed: false,
- disputed: false,
- fulfilledAt: {
- lt: threshold,
- },
- },
- select: {
- id: true,
- },
- });
-
- if (eligibleOrders.length === 0) {
- return { confirmedCount: 0 };
- }
-
- let confirmedCount = 0;
-
- for (const eligible of eligibleOrders) {
- await prisma.$transaction(async (tx) => {
- const updated = await tx.order.updateMany({
- where: {
- id: eligible.id,
- status: 'fulfilled',
- confirmed: false,
- disputed: false,
- fulfilledAt: {
- lt: threshold,
- },
- },
- data: {
- confirmed: true,
- confirmedAt: new Date(),
- confirmationSource: 'timeout',
- },
- });
-
- if (updated.count > 0) {
- confirmedCount += updated.count;
- await record(tx, SYSTEM_ACTOR, {
- entityType: 'Order',
- entityId: eligible.id,
- action: 'order.receipt_confirmed',
- fromState: 'fulfilled',
- toState: 'confirmed',
- metadata: { confirmationSource: 'timeout' },
- });
- }
- });
- }
-
- return { confirmedCount };
-}
-
-export function startOrderExpiryJob(): () => void {
- const run = async () => {
- try {
- const { confirmedCount } = await expireUnconfirmedOrders();
- if (confirmedCount > 0) {
- // eslint-disable-next-line no-console
- console.log(`[jobs] auto-confirmed ${confirmedCount} order(s) past confirmation window`);
- }
- } catch (err) {
- // eslint-disable-next-line no-console
- console.error('[jobs] order expiry job failed:', err instanceof Error ? err.name : typeof err);
- }
- };
-
- const timer = setInterval(run, config.orders.expiryIntervalSeconds * 1000);
- timer.unref();
-
- // Pick up any pending unconfirmed orders on startup
- void run();
-
- return () => {
- clearInterval(timer);
- };
-}
+import { prisma } from '../db/prisma-client';
+import { config } from '../config';
+import { record, SYSTEM_ACTOR } from '../modules/audit/services';
+
+export async function expireUnconfirmedOrders(
+ windowHours: number = config.orders.confirmationWindowHours,
+): Promise<{ confirmedCount: number }> {
+ const threshold = new Date(Date.now() - windowHours * 60 * 60 * 1000);
+
+ const eligibleOrders = await prisma.order.findMany({
+ where: {
+ status: 'fulfilled',
+ confirmed: false,
+ disputed: false,
+ fulfilledAt: {
+ lt: threshold,
+ },
+ },
+ select: {
+ id: true,
+ },
+ });
+
+ if (eligibleOrders.length === 0) {
+ return { confirmedCount: 0 };
+ }
+
+ let confirmedCount = 0;
+
+ for (const eligible of eligibleOrders) {
+ await prisma.$transaction(async (tx) => {
+ const updated = await tx.order.updateMany({
+ where: {
+ id: eligible.id,
+ status: 'fulfilled',
+ confirmed: false,
+ disputed: false,
+ fulfilledAt: {
+ lt: threshold,
+ },
+ },
+ data: {
+ confirmed: true,
+ confirmedAt: new Date(),
+ confirmationSource: 'timeout',
+ },
+ });
+
+ if (updated.count > 0) {
+ confirmedCount += updated.count;
+ await record(tx, SYSTEM_ACTOR, {
+ entityType: 'Order',
+ entityId: eligible.id,
+ action: 'order.receipt_confirmed',
+ fromState: 'fulfilled',
+ toState: 'confirmed',
+ metadata: { confirmationSource: 'timeout' },
+ });
+ }
+ });
+ }
+
+ return { confirmedCount };
+}
+
+export function startOrderExpiryJob(): () => void {
+ const run = async () => {
+ try {
+ const { confirmedCount } = await expireUnconfirmedOrders();
+ if (confirmedCount > 0) {
+ // eslint-disable-next-line no-console
+ console.log(`[jobs] auto-confirmed ${confirmedCount} order(s) past confirmation window`);
+ }
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error('[jobs] order expiry job failed:', err instanceof Error ? err.name : typeof err);
+ }
+ };
+
+ const timer = setInterval(run, config.orders.expiryIntervalSeconds * 1000);
+ timer.unref();
+
+ // Pick up any pending unconfirmed orders on startup
+ void run();
+
+ return () => {
+ clearInterval(timer);
+ };
+}
diff --git a/backend/src/jobs/payment-intents.ts b/backend/src/jobs/payment-intents.ts
index 227cab59..48587af5 100644
--- a/backend/src/jobs/payment-intents.ts
+++ b/backend/src/jobs/payment-intents.ts
@@ -1,42 +1,42 @@
-// Payment-intent jobs (Phase 5.5, issue #80). Plain interval, per
-// ARCHITECTURE_ESSENTIALS.md ("keep jobs simple until a real job exists β
-// don't pre-install a queue"). Started from src/server.ts only, so test
-// suites that build the app never start a timer; tests drive the service
-// function directly.
-//
-// The transition itself is a conditional UPDATE in the payments module
-// (`awaiting_signature -> created` only where the prepared window has
-// passed), so overlapping runs and multiple processes are safe. Expiry is a
-// normal case: the patient simply prepares again.
-import { config } from '../config';
-import { expireStalePreparedIntents } from '../modules/payments/services';
-
-export function startPaymentIntentJobs(): () => void {
- let running = false;
- const sweep = () => {
- if (running) return;
- running = true;
- void expireStalePreparedIntents()
- .then(({ expired }) => {
- if (expired > 0) {
- // eslint-disable-next-line no-console
- console.log(`[jobs] expired ${expired} stale prepared payment intent(s)`);
- }
- })
- .catch((err) => {
- // Error type only: a database error message can echo row data.
- // eslint-disable-next-line no-console
- console.error(`[jobs] payment-intent expiry sweep failed: ${err instanceof Error ? err.name : typeof err}`);
- })
- .finally(() => {
- running = false;
- });
- };
-
- const timer = setInterval(sweep, config.payments.prepareExpirySweepIntervalSeconds * 1000);
- timer.unref();
- // Pick up anything already past its window on startup.
- sweep();
-
- return () => clearInterval(timer);
-}
+// Payment-intent jobs (Phase 5.5, issue #80). Plain interval, per
+// ARCHITECTURE_ESSENTIALS.md ("keep jobs simple until a real job exists β
+// don't pre-install a queue"). Started from src/server.ts only, so test
+// suites that build the app never start a timer; tests drive the service
+// function directly.
+//
+// The transition itself is a conditional UPDATE in the payments module
+// (`awaiting_signature -> created` only where the prepared window has
+// passed), so overlapping runs and multiple processes are safe. Expiry is a
+// normal case: the patient simply prepares again.
+import { config } from '../config';
+import { expireStalePreparedIntents } from '../modules/payments/services';
+
+export function startPaymentIntentJobs(): () => void {
+ let running = false;
+ const sweep = () => {
+ if (running) return;
+ running = true;
+ void expireStalePreparedIntents()
+ .then(({ expired }) => {
+ if (expired > 0) {
+ // eslint-disable-next-line no-console
+ console.log(`[jobs] expired ${expired} stale prepared payment intent(s)`);
+ }
+ })
+ .catch((err) => {
+ // Error type only: a database error message can echo row data.
+ // eslint-disable-next-line no-console
+ console.error(`[jobs] payment-intent expiry sweep failed: ${err instanceof Error ? err.name : typeof err}`);
+ })
+ .finally(() => {
+ running = false;
+ });
+ };
+
+ const timer = setInterval(sweep, config.payments.prepareExpirySweepIntervalSeconds * 1000);
+ timer.unref();
+ // Pick up anything already past its window on startup.
+ sweep();
+
+ return () => clearInterval(timer);
+}
diff --git a/backend/src/jobs/referral-expiry.ts b/backend/src/jobs/referral-expiry.ts
index a37fbb2d..de0cff66 100644
--- a/backend/src/jobs/referral-expiry.ts
+++ b/backend/src/jobs/referral-expiry.ts
@@ -1,35 +1,35 @@
-// Referral expiry sweep (Phase 3, issue #8). A plain interval, per
-// ARCHITECTURE_ESSENTIALS.md ("keep jobs simple until a real job exists β
-// don't pre-install a queue"); move it onto a shared job runner if one is
-// ever introduced. Started from src/server.ts only, so test suites that build
-// the app never start a timer. The state change is one conditional UPDATE in
-// the referrals module, so running more than one process is safe.
-import { config } from '../config';
-import { expireStaleReferrals } from '../modules/referrals/services';
-
-export function startReferralExpiryJob(): () => void {
- let running = false;
- const run = () => {
- if (running) return;
- running = true;
- void expireStaleReferrals()
- .then(({ expired }) => {
- if (expired > 0) {
- // Count only β never ids or notes.
- // eslint-disable-next-line no-console
- console.log(`[jobs] referral expiry: ${expired} expired`);
- }
- })
- .catch((err) => {
- // eslint-disable-next-line no-console
- console.error(`[jobs] referral expiry failed: ${err instanceof Error ? err.name : typeof err}`);
- })
- .finally(() => {
- running = false;
- });
- };
- run(); // catch up immediately after a restart, then on the interval
- const timer = setInterval(run, config.referrals.expirySweepIntervalSeconds * 1000);
- timer.unref();
- return () => clearInterval(timer);
-}
+// Referral expiry sweep (Phase 3, issue #8). A plain interval, per
+// ARCHITECTURE_ESSENTIALS.md ("keep jobs simple until a real job exists β
+// don't pre-install a queue"); move it onto a shared job runner if one is
+// ever introduced. Started from src/server.ts only, so test suites that build
+// the app never start a timer. The state change is one conditional UPDATE in
+// the referrals module, so running more than one process is safe.
+import { config } from '../config';
+import { expireStaleReferrals } from '../modules/referrals/services';
+
+export function startReferralExpiryJob(): () => void {
+ let running = false;
+ const run = () => {
+ if (running) return;
+ running = true;
+ void expireStaleReferrals()
+ .then(({ expired }) => {
+ if (expired > 0) {
+ // Count only β never ids or notes.
+ // eslint-disable-next-line no-console
+ console.log(`[jobs] referral expiry: ${expired} expired`);
+ }
+ })
+ .catch((err) => {
+ // eslint-disable-next-line no-console
+ console.error(`[jobs] referral expiry failed: ${err instanceof Error ? err.name : typeof err}`);
+ })
+ .finally(() => {
+ running = false;
+ });
+ };
+ run(); // catch up immediately after a restart, then on the interval
+ const timer = setInterval(run, config.referrals.expirySweepIntervalSeconds * 1000);
+ timer.unref();
+ return () => clearInterval(timer);
+}
diff --git a/backend/src/logger.ts b/backend/src/logger.ts
index 65b41758..f6c97251 100644
--- a/backend/src/logger.ts
+++ b/backend/src/logger.ts
@@ -1,86 +1,86 @@
-// Structured JSON logging (Phase 9 β issue #30, PR #189).
-//
-// One `pino` logger for the process; request paths get a child logger
-// carrying the request id (see middleware/request-id.ts). Log level is
-// per-environment via `config.log.level` (`LOG_LEVEL` wins; `silent` under
-// NODE_ENV=test so captured output stays readable; `info` in production).
-//
-// Redaction is configured once, here, and applies to every line the
-// process emits: auth headers, cookies, passwords, tokens, and the PHI
-// field paths this platform actually holds (PRD Β§7 β patient intake and
-// clinical free text must never reach a log line). Censor is the literal
-// `[REDACTED]` so a drain can tell a redacted field from an absent one.
-import pino from 'pino';
-import { config } from './config';
-
-/** Redaction paths (pino `redact.paths` syntax). Top-level keys are listed
- * bare and again with a `*.key` wildcard so a sensitive field is censored
- * at any nesting depth, not only at the root. Keep in sync with the PHI
- * field names in ARCHITECTURE.md Β§4 β a new clinical free-text column
- * belongs here. */
-export const REDACT_PATHS = [
- // Credentials.
- 'password', '*.password',
- 'passwordHash', '*.passwordHash',
- 'token', '*.token',
- 'accessToken', '*.accessToken',
- 'refreshToken', '*.refreshToken',
- 'csrfToken', '*.csrfToken',
- // Request credentials as headers (pino redacts exact paths, so these
- // are spelled out rather than wildcarded).
- 'req.headers.authorization',
- 'req.headers.cookie',
- 'req.headers["set-cookie"]',
- // PHI field paths (PRD Β§7).
- 'intakeNotes', '*.intakeNotes',
- 'symptoms', '*.symptoms',
- 'diagnosis', '*.diagnosis',
- 'allergies', '*.allergies',
- 'medications', '*.medications',
- 'dateOfBirth', '*.dateOfBirth',
- 'ssn', '*.ssn',
-];
-
-const MAX_SERIALISER_DEPTH = 8;
-
-/** Deep-copies a value, converting every BigInt to its decimal string.
- * Payment amounts are raw stroops (BigInt), and a bare JSON.stringify of
- * one throws β which would take a log line (and in a request path, the
- * response) down with it. */
-function stringifyBigInts(value: unknown, depth = 0): unknown {
- if (typeof value === 'bigint') return value.toString();
- if (depth >= MAX_SERIALISER_DEPTH) return value;
- if (value instanceof Date || value === null || typeof value !== 'object') return value;
- if (Array.isArray(value)) return value.map((item) => stringifyBigInts(item, depth + 1));
- const out: Record = {};
- for (const [key, entry] of Object.entries(value as Record)) {
- out[key] = stringifyBigInts(entry, depth + 1);
- }
- return out;
-}
-
-/** Serialisers for the keys that can legitimately carry BigInt values.
- * pino applies a serialiser wherever the key appears, at any depth. */
-export const bigIntSerialiser = {
- amount: stringifyBigInts,
- amountStroops: stringifyBigInts,
- totalStroops: stringifyBigInts,
- stroops: stringifyBigInts,
-};
-
-export const loggerOptions: pino.LoggerOptions = {
- level: config.log.level,
- redact: { paths: REDACT_PATHS, censor: '[REDACTED]' },
- serializers: bigIntSerialiser,
-};
-
-/** The process-wide logger. Non-request code (jobs, startup) logs through
- * this; request code gets a child via `requestLogger`. */
-export const logger = pino(loggerOptions);
-
-/** Request-scoped child logger: every line it emits carries the request id
- * that `X-Request-Id` responses and `AuditEvent` rows also carry, so one
- * id correlates an HTTP request, its logs and its audit trail. */
-export function requestLogger(bindings: { requestId: string }): pino.Logger {
- return logger.child({ requestId: bindings.requestId });
-}
+// Structured JSON logging (Phase 9 β issue #30, PR #189).
+//
+// One `pino` logger for the process; request paths get a child logger
+// carrying the request id (see middleware/request-id.ts). Log level is
+// per-environment via `config.log.level` (`LOG_LEVEL` wins; `silent` under
+// NODE_ENV=test so captured output stays readable; `info` in production).
+//
+// Redaction is configured once, here, and applies to every line the
+// process emits: auth headers, cookies, passwords, tokens, and the PHI
+// field paths this platform actually holds (PRD Β§7 β patient intake and
+// clinical free text must never reach a log line). Censor is the literal
+// `[REDACTED]` so a drain can tell a redacted field from an absent one.
+import pino from 'pino';
+import { config } from './config';
+
+/** Redaction paths (pino `redact.paths` syntax). Top-level keys are listed
+ * bare and again with a `*.key` wildcard so a sensitive field is censored
+ * at any nesting depth, not only at the root. Keep in sync with the PHI
+ * field names in ARCHITECTURE.md Β§4 β a new clinical free-text column
+ * belongs here. */
+export const REDACT_PATHS = [
+ // Credentials.
+ 'password', '*.password',
+ 'passwordHash', '*.passwordHash',
+ 'token', '*.token',
+ 'accessToken', '*.accessToken',
+ 'refreshToken', '*.refreshToken',
+ 'csrfToken', '*.csrfToken',
+ // Request credentials as headers (pino redacts exact paths, so these
+ // are spelled out rather than wildcarded).
+ 'req.headers.authorization',
+ 'req.headers.cookie',
+ 'req.headers["set-cookie"]',
+ // PHI field paths (PRD Β§7).
+ 'intakeNotes', '*.intakeNotes',
+ 'symptoms', '*.symptoms',
+ 'diagnosis', '*.diagnosis',
+ 'allergies', '*.allergies',
+ 'medications', '*.medications',
+ 'dateOfBirth', '*.dateOfBirth',
+ 'ssn', '*.ssn',
+];
+
+const MAX_SERIALISER_DEPTH = 8;
+
+/** Deep-copies a value, converting every BigInt to its decimal string.
+ * Payment amounts are raw stroops (BigInt), and a bare JSON.stringify of
+ * one throws β which would take a log line (and in a request path, the
+ * response) down with it. */
+function stringifyBigInts(value: unknown, depth = 0): unknown {
+ if (typeof value === 'bigint') return value.toString();
+ if (depth >= MAX_SERIALISER_DEPTH) return value;
+ if (value instanceof Date || value === null || typeof value !== 'object') return value;
+ if (Array.isArray(value)) return value.map((item) => stringifyBigInts(item, depth + 1));
+ const out: Record = {};
+ for (const [key, entry] of Object.entries(value as Record)) {
+ out[key] = stringifyBigInts(entry, depth + 1);
+ }
+ return out;
+}
+
+/** Serialisers for the keys that can legitimately carry BigInt values.
+ * pino applies a serialiser wherever the key appears, at any depth. */
+export const bigIntSerialiser = {
+ amount: stringifyBigInts,
+ amountStroops: stringifyBigInts,
+ totalStroops: stringifyBigInts,
+ stroops: stringifyBigInts,
+};
+
+export const loggerOptions: pino.LoggerOptions = {
+ level: config.log.level,
+ redact: { paths: REDACT_PATHS, censor: '[REDACTED]' },
+ serializers: bigIntSerialiser,
+};
+
+/** The process-wide logger. Non-request code (jobs, startup) logs through
+ * this; request code gets a child via `requestLogger`. */
+export const logger = pino(loggerOptions);
+
+/** Request-scoped child logger: every line it emits carries the request id
+ * that `X-Request-Id` responses and `AuditEvent` rows also carry, so one
+ * id correlates an HTTP request, its logs and its audit trail. */
+export function requestLogger(bindings: { requestId: string }): pino.Logger {
+ return logger.child({ requestId: bindings.requestId });
+}
diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts
index 34a66d59..191344db 100644
--- a/backend/src/middleware/auth.ts
+++ b/backend/src/middleware/auth.ts
@@ -1,67 +1,67 @@
-import type { NextFunction, Request, Response } from 'express';
-import { assertActiveSession, assertEmailVerified, verifyAccessToken } from '../modules/auth/services';
-import { HttpError } from '../http-error';
-import type { Role } from '@prisma/client';
-
-declare global {
- // eslint-disable-next-line @typescript-eslint/no-namespace
- namespace Express {
- interface Request {
- user?: { id: string; role: Role; locale?: string };
- }
- }
-}
-
-/** Verifies the `Authorization: Bearer ` header and attaches
- * `req.user`.
- *
- * Throws 401 if missing/invalid/expired. Beyond expiry, `assertActiveSession`
- * is a database read per request that rejects a deactivated account and any
- * token minted before that account's sessions were invalidated (Phase 8,
- * issue #39) β without it, a deactivated user's access token would keep
- * working for the rest of its 15-minute TTL. This is not a general
- * revocation list; that is still issue #18. */
-export async function authenticate(req: Request, _res: Response, next: NextFunction) {
- const header = req.headers.authorization;
- if (!header?.startsWith('Bearer ')) {
- throw new HttpError(401, 'Missing or malformed Authorization header.');
- }
-
- let payload;
- try {
- payload = verifyAccessToken(header.slice('Bearer '.length));
- } catch {
- throw new HttpError(401, 'Invalid or expired access token.');
- }
-
- await assertActiveSession(payload.sub, payload.iat);
- req.user = { id: payload.sub, role: payload.role, locale: payload.locale };
- next();
-}
-
-/** Must run after `authenticate`. Rejects with `403 EMAIL_NOT_VERIFIED`
- * (Phase 8, issue #34) when the caller's own address hasn't been verified.
- * Applied only to mutations that need a trustworthy contact address β
- * booking and payments β never to login/browsing.
- *
- * This is a DB read, so verifying the address in another tab unblocks the
- * caller immediately, instead of after their access token's 15-minute TTL.
- * It is deliberately *not* the doctor/facility `verificationStatus` gate:
- * that is a separate, manual credential review (Β§4.1). */
-export async function requireVerifiedEmail(req: Request, _res: Response, next: NextFunction) {
- if (!req.user) throw new HttpError(401, 'Authentication required.');
- await assertEmailVerified(req.user.id);
- next();
-}
-
-/** Must run after `authenticate`. Rejects with 403 if the authenticated
- * user's role isn't in the allowed set. */
-export function requireRole(...allowedRoles: Role[]) {
- return (req: Request, _res: Response, next: NextFunction) => {
- if (!req.user) throw new HttpError(401, 'Authentication required.');
- if (!allowedRoles.includes(req.user.role)) {
- throw new HttpError(403, 'You do not have permission to perform this action.');
- }
- next();
- };
-}
+import type { NextFunction, Request, Response } from 'express';
+import { assertActiveSession, assertEmailVerified, verifyAccessToken } from '../modules/auth/services';
+import { HttpError } from '../http-error';
+import type { Role } from '@prisma/client';
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Express {
+ interface Request {
+ user?: { id: string; role: Role; locale?: string };
+ }
+ }
+}
+
+/** Verifies the `Authorization: Bearer ` header and attaches
+ * `req.user`.
+ *
+ * Throws 401 if missing/invalid/expired. Beyond expiry, `assertActiveSession`
+ * is a database read per request that rejects a deactivated account and any
+ * token minted before that account's sessions were invalidated (Phase 8,
+ * issue #39) β without it, a deactivated user's access token would keep
+ * working for the rest of its 15-minute TTL. This is not a general
+ * revocation list; that is still issue #18. */
+export async function authenticate(req: Request, _res: Response, next: NextFunction) {
+ const header = req.headers.authorization;
+ if (!header?.startsWith('Bearer ')) {
+ throw new HttpError(401, 'Missing or malformed Authorization header.');
+ }
+
+ let payload;
+ try {
+ payload = verifyAccessToken(header.slice('Bearer '.length));
+ } catch {
+ throw new HttpError(401, 'Invalid or expired access token.');
+ }
+
+ await assertActiveSession(payload.sub, payload.iat);
+ req.user = { id: payload.sub, role: payload.role, locale: payload.locale };
+ next();
+}
+
+/** Must run after `authenticate`. Rejects with `403 EMAIL_NOT_VERIFIED`
+ * (Phase 8, issue #34) when the caller's own address hasn't been verified.
+ * Applied only to mutations that need a trustworthy contact address β
+ * booking and payments β never to login/browsing.
+ *
+ * This is a DB read, so verifying the address in another tab unblocks the
+ * caller immediately, instead of after their access token's 15-minute TTL.
+ * It is deliberately *not* the doctor/facility `verificationStatus` gate:
+ * that is a separate, manual credential review (Β§4.1). */
+export async function requireVerifiedEmail(req: Request, _res: Response, next: NextFunction) {
+ if (!req.user) throw new HttpError(401, 'Authentication required.');
+ await assertEmailVerified(req.user.id);
+ next();
+}
+
+/** Must run after `authenticate`. Rejects with 403 if the authenticated
+ * user's role isn't in the allowed set. */
+export function requireRole(...allowedRoles: Role[]) {
+ return (req: Request, _res: Response, next: NextFunction) => {
+ if (!req.user) throw new HttpError(401, 'Authentication required.');
+ if (!allowedRoles.includes(req.user.role)) {
+ throw new HttpError(403, 'You do not have permission to perform this action.');
+ }
+ next();
+ };
+}
diff --git a/backend/src/middleware/error-handler.ts b/backend/src/middleware/error-handler.ts
index 18de031f..5e346db2 100644
--- a/backend/src/middleware/error-handler.ts
+++ b/backend/src/middleware/error-handler.ts
@@ -1,46 +1,46 @@
-import type { NextFunction, Request, Response } from 'express';
-import { ZodError } from 'zod';
-import { Prisma } from '@prisma/client';
-import { HttpError } from '../http-error';
-import type { Logger } from 'pino';
-import { logger as rootLogger } from '../logger';
-import { localizedErrorMessage, resolveLocale } from '../i18n';
-
-// docforum-web's api-client.ts assumes an error envelope of
-// `{ message: string }` (documented as unconfirmed there) β this is the
-// implementation that assumption is checked against. Keep them in sync.
-export function errorHandler(err: unknown, req: Request, res: Response, _next: NextFunction) {
- const requestLogger: Logger =
- (req as Request & { log?: Logger }).log ?? rootLogger;
-
- if (err instanceof HttpError) {
- // `code` is only present when the thrower supplied one β the envelope
- // for every existing error is unchanged (`{ message }`).
- if (err.status >= 500) {
- requestLogger.error({ err }, 'Request failed');
- } else {
- requestLogger.warn({ err }, 'Request rejected');
- }
- const locale = resolveLocale(req.user?.locale, req.headers['accept-language']);
- const message = localizedErrorMessage(locale, err.code, err.message);
- res.status(err.status).json(err.code ? { message, code: err.code } : { message });
- return;
- }
-
- if (err instanceof ZodError) {
- requestLogger.warn({ err }, 'Request validation failed');
- res.status(400).json({ message: err.issues.map((issue) => issue.message).join('; ') });
- return;
- }
-
- if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002') {
- requestLogger.warn({ err }, 'Unique constraint violation');
- res.status(409).json({ message: 'A record with this value already exists.' });
- return;
- }
-
- // Log the full error (including stack) server-side only. The response
- // body is the safe envelope and never leaks internal details.
- requestLogger.error({ err }, 'Unhandled error');
- res.status(500).json({ message: 'Internal server error.' });
-}
+import type { NextFunction, Request, Response } from 'express';
+import { ZodError } from 'zod';
+import { Prisma } from '@prisma/client';
+import { HttpError } from '../http-error';
+import type { Logger } from 'pino';
+import { logger as rootLogger } from '../logger';
+import { localizedErrorMessage, resolveLocale } from '../i18n';
+
+// docforum-web's api-client.ts assumes an error envelope of
+// `{ message: string }` (documented as unconfirmed there) β this is the
+// implementation that assumption is checked against. Keep them in sync.
+export function errorHandler(err: unknown, req: Request, res: Response, _next: NextFunction) {
+ const requestLogger: Logger =
+ (req as Request & { log?: Logger }).log ?? rootLogger;
+
+ if (err instanceof HttpError) {
+ // `code` is only present when the thrower supplied one β the envelope
+ // for every existing error is unchanged (`{ message }`).
+ if (err.status >= 500) {
+ requestLogger.error({ err }, 'Request failed');
+ } else {
+ requestLogger.warn({ err }, 'Request rejected');
+ }
+ const locale = resolveLocale(req.user?.locale, req.headers['accept-language']);
+ const message = localizedErrorMessage(locale, err.code, err.message);
+ res.status(err.status).json(err.code ? { message, code: err.code } : { message });
+ return;
+ }
+
+ if (err instanceof ZodError) {
+ requestLogger.warn({ err }, 'Request validation failed');
+ res.status(400).json({ message: err.issues.map((issue) => issue.message).join('; ') });
+ return;
+ }
+
+ if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002') {
+ requestLogger.warn({ err }, 'Unique constraint violation');
+ res.status(409).json({ message: 'A record with this value already exists.' });
+ return;
+ }
+
+ // Log the full error (including stack) server-side only. The response
+ // body is the safe envelope and never leaks internal details.
+ requestLogger.error({ err }, 'Unhandled error');
+ res.status(500).json({ message: 'Internal server error.' });
+}
diff --git a/backend/src/middleware/request-id.ts b/backend/src/middleware/request-id.ts
index 8ad794a1..6be69305 100644
--- a/backend/src/middleware/request-id.ts
+++ b/backend/src/middleware/request-id.ts
@@ -1,49 +1,49 @@
-// Request ids (Phase 8, issue #43). Every request gets one, so an audit
-// event can be tied back to the HTTP request (and its logs) that caused it.
-// An inbound `X-Request-Id` (e.g. from Render's proxy or a client) is kept
-// when it's a sane token; otherwise a fresh UUID is minted. Echoed back in
-// the response header either way.
-//
-// Phase 9, issue #46: this middleware also attaches a request-scoped
-// child logger to `req.log`, so controllers and the error handler can log with
-// the request id attached without having to pluck it out of the request
-// themselves.
-import type { NextFunction, Request, Response } from 'express';
-import { randomUUID } from 'node:crypto';
-import type { AuditActor } from '../modules/audit/services';
-import type { Logger } from 'pino';
-import { requestLogger } from '../logger';
-
-declare global {
- // eslint-disable-next-line @typescript-eslint/no-namespace
- namespace Express {
- interface Request {
- requestId?: string;
- // Request-scoped child logger (Phase 9, issue #46). Always set by
- // the requestId middleware below.
- log?: Logger;
- }
- }
-}
-
-export const REQUEST_ID_HEADER = 'x-request-id';
-
-// Bounded charset and length: the value is stored and logged, so it must
-// not be a vehicle for arbitrary client data.
-const ACCEPTABLE_REQUEST_ID = /^[A-Za-z0-9._:-]{8,128}$/;
-
-export function requestId(req: Request, res: Response, next: NextFunction) {
- const inbound = req.get(REQUEST_ID_HEADER);
- const id = inbound && ACCEPTABLE_REQUEST_ID.test(inbound) ? inbound : randomUUID();
- req.requestId = id;
- req.log = requestLogger({ requestId: id });
- res.set('X-Request-Id', id);
- next();
-}
-
-/** The audit actor for an authenticated request: who, in which role, under
- * which request id. Controllers pass this into the service whose state
- * change is being audited. */
-export function auditActor(req: Request): AuditActor {
- return { userId: req.user?.id ?? null, role: req.user?.role ?? null, requestId: req.requestId ?? null };
-}
+// Request ids (Phase 8, issue #43). Every request gets one, so an audit
+// event can be tied back to the HTTP request (and its logs) that caused it.
+// An inbound `X-Request-Id` (e.g. from Render's proxy or a client) is kept
+// when it's a sane token; otherwise a fresh UUID is minted. Echoed back in
+// the response header either way.
+//
+// Phase 9, issue #46: this middleware also attaches a request-scoped
+// child logger to `req.log`, so controllers and the error handler can log with
+// the request id attached without having to pluck it out of the request
+// themselves.
+import type { NextFunction, Request, Response } from 'express';
+import { randomUUID } from 'node:crypto';
+import type { AuditActor } from '../modules/audit/services';
+import type { Logger } from 'pino';
+import { requestLogger } from '../logger';
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Express {
+ interface Request {
+ requestId?: string;
+ // Request-scoped child logger (Phase 9, issue #46). Always set by
+ // the requestId middleware below.
+ log?: Logger;
+ }
+ }
+}
+
+export const REQUEST_ID_HEADER = 'x-request-id';
+
+// Bounded charset and length: the value is stored and logged, so it must
+// not be a vehicle for arbitrary client data.
+const ACCEPTABLE_REQUEST_ID = /^[A-Za-z0-9._:-]{8,128}$/;
+
+export function requestId(req: Request, res: Response, next: NextFunction) {
+ const inbound = req.get(REQUEST_ID_HEADER);
+ const id = inbound && ACCEPTABLE_REQUEST_ID.test(inbound) ? inbound : randomUUID();
+ req.requestId = id;
+ req.log = requestLogger({ requestId: id });
+ res.set('X-Request-Id', id);
+ next();
+}
+
+/** The audit actor for an authenticated request: who, in which role, under
+ * which request id. Controllers pass this into the service whose state
+ * change is being audited. */
+export function auditActor(req: Request): AuditActor {
+ return { userId: req.user?.id ?? null, role: req.user?.role ?? null, requestId: req.requestId ?? null };
+}
diff --git a/backend/src/modules/appointments/controllers/index.ts b/backend/src/modules/appointments/controllers/index.ts
index 6a0004dc..2bf96f4d 100644
--- a/backend/src/modules/appointments/controllers/index.ts
+++ b/backend/src/modules/appointments/controllers/index.ts
@@ -1,43 +1,43 @@
-import type { Request, Response } from 'express';
-import * as appointmentsService from '../services';
-import { bookSlotSchema, listMineQuerySchema, updateStatusSchema } from '../validation';
-import { HttpError } from '../../../http-error';
-import { auditActor } from '../../../middleware/request-id';
-import { getParam } from '../../../request-params';
-
-export async function book(req: Request, res: Response) {
- if (!req.user) throw new HttpError(401, 'Authentication required.');
- const { availabilitySlotId, patientProfileId } = bookSlotSchema.parse(req.body);
- const appointment = await appointmentsService.bookSlot(auditActor(req), availabilitySlotId, patientProfileId);
- res.status(201).json(appointment);
-}
-
-export async function listMine(req: Request, res: Response) {
- if (!req.user) throw new HttpError(401, 'Authentication required.');
- const { patientProfileId } = listMineQuerySchema.parse(req.query);
- const appointments = await appointmentsService.listMyAppointments(req.user.id, patientProfileId);
- res.status(200).json(appointments);
-}
-
-export async function listDoctorToday(req: Request, res: Response) {
- if (!req.user) throw new HttpError(401, 'Authentication required.');
- const appointments = await appointmentsService.listDoctorAppointmentsToday(req.user.id);
- res.status(200).json(appointments);
-}
-
-export async function updateStatus(req: Request, res: Response) {
- if (!req.user) throw new HttpError(401, 'Authentication required.');
- const { status } = updateStatusSchema.parse(req.body);
- const appointment = await appointmentsService.updateStatusByDoctor(
- req.user.id,
- getParam(req, 'appointmentId'),
- status,
- );
- res.status(200).json(appointment);
-}
-
-export async function cancelMine(req: Request, res: Response) {
- if (!req.user) throw new HttpError(401, 'Authentication required.');
- const appointment = await appointmentsService.cancelByPatient(req.user.id, getParam(req, 'appointmentId'));
- res.status(200).json(appointment);
-}
+import type { Request, Response } from 'express';
+import * as appointmentsService from '../services';
+import { bookSlotSchema, listMineQuerySchema, updateStatusSchema } from '../validation';
+import { HttpError } from '../../../http-error';
+import { auditActor } from '../../../middleware/request-id';
+import { getParam } from '../../../request-params';
+
+export async function book(req: Request, res: Response) {
+ if (!req.user) throw new HttpError(401, 'Authentication required.');
+ const { availabilitySlotId, patientProfileId } = bookSlotSchema.parse(req.body);
+ const appointment = await appointmentsService.bookSlot(auditActor(req), availabilitySlotId, patientProfileId);
+ res.status(201).json(appointment);
+}
+
+export async function listMine(req: Request, res: Response) {
+ if (!req.user) throw new HttpError(401, 'Authentication required.');
+ const { patientProfileId } = listMineQuerySchema.parse(req.query);
+ const appointments = await appointmentsService.listMyAppointments(req.user.id, patientProfileId);
+ res.status(200).json(appointments);
+}
+
+export async function listDoctorToday(req: Request, res: Response) {
+ if (!req.user) throw new HttpError(401, 'Authentication required.');
+ const appointments = await appointmentsService.listDoctorAppointmentsToday(req.user.id);
+ res.status(200).json(appointments);
+}
+
+export async function updateStatus(req: Request, res: Response) {
+ if (!req.user) throw new HttpError(401, 'Authentication required.');
+ const { status } = updateStatusSchema.parse(req.body);
+ const appointment = await appointmentsService.updateStatusByDoctor(
+ req.user.id,
+ getParam(req, 'appointmentId'),
+ status,
+ );
+ res.status(200).json(appointment);
+}
+
+export async function cancelMine(req: Request, res: Response) {
+ if (!req.user) throw new HttpError(401, 'Authentication required.');
+ const appointment = await appointmentsService.cancelByPatient(req.user.id, getParam(req, 'appointmentId'));
+ res.status(200).json(appointment);
+}
diff --git a/backend/src/modules/appointments/intake.ts b/backend/src/modules/appointments/intake.ts
index 09c516b1..dfb2dfbd 100644
--- a/backend/src/modules/appointments/intake.ts
+++ b/backend/src/modules/appointments/intake.ts
@@ -1,2 +1,2 @@
-export type IntakePayload={symptoms:string;allergies?:string[];medications?:string[];notes?:string};
-export function validateIntake(value:unknown):IntakePayload{if(!value||typeof value!=="object")throw new Error("intake is required before booking");const v=value as Record;if(typeof v.symptoms!=="string"||!v.symptoms.trim())throw new Error("intake.symptoms is required");if(v.allergies!==undefined&&!Array.isArray(v.allergies))throw new Error("intake.allergies must be an array");return {symptoms:v.symptoms.trim(),allergies:v.allergies as string[]|undefined,medications:v.medications as string[]|undefined,notes:typeof v.notes==="string"?v.notes.trim():undefined};}
+export type IntakePayload={symptoms:string;allergies?:string[];medications?:string[];notes?:string};
+export function validateIntake(value:unknown):IntakePayload{if(!value||typeof value!=="object")throw new Error("intake is required before booking");const v=value as Record;if(typeof v.symptoms!=="string"||!v.symptoms.trim())throw new Error("intake.symptoms is required");if(v.allergies!==undefined&&!Array.isArray(v.allergies))throw new Error("intake.allergies must be an array");return {symptoms:v.symptoms.trim(),allergies:v.allergies as string[]|undefined,medications:v.medications as string[]|undefined,notes:typeof v.notes==="string"?v.notes.trim():undefined};}
diff --git a/backend/src/modules/appointments/repositories/index.ts b/backend/src/modules/appointments/repositories/index.ts
index 7f7316af..d134bd95 100644
--- a/backend/src/modules/appointments/repositories/index.ts
+++ b/backend/src/modules/appointments/repositories/index.ts
@@ -1,89 +1,89 @@
-import { prisma } from '../../../db/prisma-client';
-import type { AppointmentStatus, Prisma } from '@prisma/client';
-
-type DbClient = typeof prisma | Prisma.TransactionClient;
-
-export function createAppointment(
- db: DbClient,
- data: {
- patientProfileId: string;
- doctorProfileId: string;
- availabilitySlotId: string;
- origin?: 'direct_booking' | 'referral';
- referralId?: string;
- },
-) {
- return db.appointment.create({ data });
-}
-
-/** One keyset page of a patient's appointments, oldest first (patient data
- * export, issue #40). Appointment columns only β slot/doctor details come
- * from those modules' services, not a join. */
-export function findAppointmentsPageForPatient(patientProfileId: string, cursor: string | undefined, take: number) {
- return prisma.appointment.findMany({
- where: { patientProfileId },
- orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
- take,
- ...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
- });
-}
-
-export function findAppointmentsForPatient(patientProfileId: string) {
- return prisma.appointment.findMany({
- where: { patientProfileId },
- include: { availabilitySlot: true },
- orderBy: { createdAt: 'desc' },
- });
-}
-
-/** Today's scheduled appointments for one doctor's own calendar. The
- * caller supplies the server-local day bounds; no patient-module data is
- * joined here, preserving the module boundary. */
-export function findAppointmentsForDoctorOnDay(doctorProfileId: string, start: Date, end: Date) {
- return prisma.appointment.findMany({
- where: {
- doctorProfileId,
- status: 'scheduled',
- availabilitySlot: { is: { startTime: { gte: start, lt: end } } },
- },
- include: { availabilitySlot: true },
- orderBy: [{ availabilitySlot: { startTime: 'asc' } }, { id: 'asc' }],
- });
-}
-
-/** Minimal appointment context for a doctor-owned consultation. */
-export function findAppointmentDoctorContext(id: string) {
- return prisma.appointment.findUnique({ where: { id }, include: { doctorProfile: { select: { id: true, userId: true, verificationStatus: true } } } });
-}
-
-export function findAppointmentById(db: DbClient, id: string) {
- return db.appointment.findUnique({ where: { id } });
-}
-
-/**
- * The appointment state machine's only write path: a conditional UPDATE
- * that matches `status = 'scheduled'`. Exactly one concurrent caller can
- * move a given appointment out of `scheduled`; a second attempt (or a
- * replayed hook) matches zero rows, which the service turns into a 409 β
- * this is what makes the referral cascade (issue #67) at-most-once per
- * real transition, without relying on app-level checks.
- */
-export function updateStatusIfScheduled(db: DbClient, id: string, status: AppointmentStatus) {
- return db.appointment.updateMany({ where: { id, status: 'scheduled' }, data: { status } });
-}
-
-/**
- * A cancelled appointment that released its slot back to `open` must also
- * drop its unique claim on availabilitySlotId β otherwise re-booking the
- * freed slot hits the unique constraint (discovered by the cascade
- * integration tests). The history row survives; only the slot link is
- * cleared.
- */
-export function clearAvailabilitySlotLink(db: DbClient, id: string) {
- return db.appointment.updateMany({ where: { id }, data: { availabilitySlotId: null } });
-}
-
-/** Existence check for the account-deletion retention hold (issue #41). */
-export async function patientHasAppointments(patientProfileId: string): Promise {
- return (await prisma.appointment.findFirst({ where: { patientProfileId }, select: { id: true } })) !== null;
-}
+import { prisma } from '../../../db/prisma-client';
+import type { AppointmentStatus, Prisma } from '@prisma/client';
+
+type DbClient = typeof prisma | Prisma.TransactionClient;
+
+export function createAppointment(
+ db: DbClient,
+ data: {
+ patientProfileId: string;
+ doctorProfileId: string;
+ availabilitySlotId: string;
+ origin?: 'direct_booking' | 'referral';
+ referralId?: string;
+ },
+) {
+ return db.appointment.create({ data });
+}
+
+/** One keyset page of a patient's appointments, oldest first (patient data
+ * export, issue #40). Appointment columns only β slot/doctor details come
+ * from those modules' services, not a join. */
+export function findAppointmentsPageForPatient(patientProfileId: string, cursor: string | undefined, take: number) {
+ return prisma.appointment.findMany({
+ where: { patientProfileId },
+ orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
+ take,
+ ...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
+ });
+}
+
+export function findAppointmentsForPatient(patientProfileId: string) {
+ return prisma.appointment.findMany({
+ where: { patientProfileId },
+ include: { availabilitySlot: true },
+ orderBy: { createdAt: 'desc' },
+ });
+}
+
+/** Today's scheduled appointments for one doctor's own calendar. The
+ * caller supplies the server-local day bounds; no patient-module data is
+ * joined here, preserving the module boundary. */
+export function findAppointmentsForDoctorOnDay(doctorProfileId: string, start: Date, end: Date) {
+ return prisma.appointment.findMany({
+ where: {
+ doctorProfileId,
+ status: 'scheduled',
+ availabilitySlot: { is: { startTime: { gte: start, lt: end } } },
+ },
+ include: { availabilitySlot: true },
+ orderBy: [{ availabilitySlot: { startTime: 'asc' } }, { id: 'asc' }],
+ });
+}
+
+/** Minimal appointment context for a doctor-owned consultation. */
+export function findAppointmentDoctorContext(id: string) {
+ return prisma.appointment.findUnique({ where: { id }, include: { doctorProfile: { select: { id: true, userId: true, verificationStatus: true } } } });
+}
+
+export function findAppointmentById(db: DbClient, id: string) {
+ return db.appointment.findUnique({ where: { id } });
+}
+
+/**
+ * The appointment state machine's only write path: a conditional UPDATE
+ * that matches `status = 'scheduled'`. Exactly one concurrent caller can
+ * move a given appointment out of `scheduled`; a second attempt (or a
+ * replayed hook) matches zero rows, which the service turns into a 409 β
+ * this is what makes the referral cascade (issue #67) at-most-once per
+ * real transition, without relying on app-level checks.
+ */
+export function updateStatusIfScheduled(db: DbClient, id: string, status: AppointmentStatus) {
+ return db.appointment.updateMany({ where: { id, status: 'scheduled' }, data: { status } });
+}
+
+/**
+ * A cancelled appointment that released its slot back to `open` must also
+ * drop its unique claim on availabilitySlotId β otherwise re-booking the
+ * freed slot hits the unique constraint (discovered by the cascade
+ * integration tests). The history row survives; only the slot link is
+ * cleared.
+ */
+export function clearAvailabilitySlotLink(db: DbClient, id: string) {
+ return db.appointment.updateMany({ where: { id }, data: { availabilitySlotId: null } });
+}
+
+/** Existence check for the account-deletion retention hold (issue #41). */
+export async function patientHasAppointments(patientProfileId: string): Promise {
+ return (await prisma.appointment.findFirst({ where: { patientProfileId }, select: { id: true } })) !== null;
+}
diff --git a/backend/src/modules/appointments/routes/index.ts b/backend/src/modules/appointments/routes/index.ts
index dac47c1f..9868e58a 100644
--- a/backend/src/modules/appointments/routes/index.ts
+++ b/backend/src/modules/appointments/routes/index.ts
@@ -1,34 +1,34 @@
-import { Router } from 'express';
-import { authenticate, requireRole, requireVerifiedEmail } from '../../../middleware/auth';
-import * as appointmentsController from '../controllers';
-
-export const appointmentsRouter = Router();
-
-// Booking is a mutation against someone else's calendar, and the patient
-// gets notifications about it β so it requires a verified address
-// (Phase 8, issue #34). Browsing (`GET /availability/...`) stays open.
-appointmentsRouter.post(
- '/',
- authenticate,
- requireRole('patient'),
- requireVerifiedEmail,
- appointmentsController.book,
-);
-appointmentsRouter.get('/mine', authenticate, requireRole('patient'), appointmentsController.listMine);
-appointmentsRouter.get('/doctor/today', authenticate, requireRole('doctor'), appointmentsController.listDoctorToday);
-
-// Lifecycle (issue #3 + the cascade trigger for issue #67): only the
-// treating doctor moves an appointment through completed/cancelled/
-// no_show; the patient may cancel their own β nothing else.
-appointmentsRouter.patch(
- '/:appointmentId/status',
- authenticate,
- requireRole('doctor'),
- appointmentsController.updateStatus,
-);
-appointmentsRouter.patch(
- '/:appointmentId/cancel',
- authenticate,
- requireRole('patient'),
- appointmentsController.cancelMine,
-);
+import { Router } from 'express';
+import { authenticate, requireRole, requireVerifiedEmail } from '../../../middleware/auth';
+import * as appointmentsController from '../controllers';
+
+export const appointmentsRouter = Router();
+
+// Booking is a mutation against someone else's calendar, and the patient
+// gets notifications about it β so it requires a verified address
+// (Phase 8, issue #34). Browsing (`GET /availability/...`) stays open.
+appointmentsRouter.post(
+ '/',
+ authenticate,
+ requireRole('patient'),
+ requireVerifiedEmail,
+ appointmentsController.book,
+);
+appointmentsRouter.get('/mine', authenticate, requireRole('patient'), appointmentsController.listMine);
+appointmentsRouter.get('/doctor/today', authenticate, requireRole('doctor'), appointmentsController.listDoctorToday);
+
+// Lifecycle (issue #3 + the cascade trigger for issue #67): only the
+// treating doctor moves an appointment through completed/cancelled/
+// no_show; the patient may cancel their own β nothing else.
+appointmentsRouter.patch(
+ '/:appointmentId/status',
+ authenticate,
+ requireRole('doctor'),
+ appointmentsController.updateStatus,
+);
+appointmentsRouter.patch(
+ '/:appointmentId/cancel',
+ authenticate,
+ requireRole('patient'),
+ appointmentsController.cancelMine,
+);
diff --git a/backend/src/modules/appointments/services/index.ts b/backend/src/modules/appointments/services/index.ts
index 7b788880..a415d297 100644
--- a/backend/src/modules/appointments/services/index.ts
+++ b/backend/src/modules/appointments/services/index.ts
@@ -1,326 +1,326 @@
-import { prisma } from '../../../db/prisma-client';
-import { HttpError } from '../../../http-error';
-import type { AppointmentStatus, Prisma } from '@prisma/client';
-import { transitionAppointmentStatus } from '../state-machine';
-import * as appointmentsRepo from '../repositories';
-import * as availabilityService from '../../availability/services';
-import * as doctorsService from '../../doctors/services';
-import * as patientsService from '../../patients/services';
-import * as auditService from '../../audit/services';
-import * as referralsService from '../../referrals/services';
-
-/**
- * Books a slot for the calling patient. This is the correctness-critical
- * path in the whole backend (ARCHITECTURE.md Β§5.1 / ROADMAP.md Phase 1 β
- * "the single highest-priority correctness item in the whole roadmap").
- *
- * Two steps, one transaction:
- * 1. `availabilityService.claimOpenSlot` β a conditional `UPDATE ...
- * WHERE status = 'open'`, atomic at the DB level. Exactly one
- * concurrent caller can ever see `count === 1` for a given slot.
- * 2. Create the `Appointment` row.
- * Both inside the same transaction so a failure between them can never
- * leave a slot marked `booked` with no `Appointment` to show for it.
- *
- * See backend/tests/integration/booking.test.ts for the concurrency proof
- * this is built to satisfy.
- *
- * Phase 8, issue #42 (docs/adr/0011): `patientProfileId` books for a
- * dependent the caller is an active guardian of. Authorisation happens
- * here, BEFORE the transaction β it only decides which profile the
- * appointment is for. The transaction below is unchanged by it.
- *
- * Phase 8, issue #43: the booking's AuditEvent is written as a third step
- * of the same transaction, after the Appointment row exists β so an event
- * exists if and only if the booking committed. A caller that loses the
- * race fails at step 1 and never reaches it. For a guardian booking, the
- * actor is the guardian and the event's metadata names the dependent's
- * profile id.
- */
-export async function bookSlot(actor: auditService.AuditActor, availabilitySlotId: string, patientProfileId?: string) {
- if (!actor.userId) throw new HttpError(401, 'Authentication required.');
- const patientProfile = await patientsService.resolveActingProfile(actor.userId, patientProfileId);
-
- return prisma.$transaction(async (tx) => {
- const claimed = await availabilityService.claimOpenSlot(tx, availabilitySlotId);
- if (!claimed) {
- throw new HttpError(409, 'This slot is no longer available.');
- }
-
- const slot = await availabilityService.getSlot(tx, availabilitySlotId);
- if (!slot) {
- // Can't happen in practice (claimOpenSlot just matched this id), but
- // keeps TypeScript honest and fails loudly instead of silently.
- throw new HttpError(404, 'Slot not found.');
- }
-
- const appointment = await appointmentsRepo.createAppointment(tx, {
- patientProfileId: patientProfile.id,
- doctorProfileId: slot.doctorProfileId,
- availabilitySlotId,
- });
-
- await auditService.record(tx, actor, {
- entityType: 'Appointment',
- entityId: appointment.id,
- action: 'appointment.booked',
- fromState: null,
- toState: appointment.status,
- metadata: {
- availabilitySlotId,
- doctorProfileId: slot.doctorProfileId,
- patientProfileId: patientProfile.id,
- origin: appointment.origin,
- },
- });
-
- return appointment;
- });
-}
-
-export async function listMyAppointments(userId: string, patientProfileId?: string) {
- const patientProfile = await patientsService.resolveActingProfile(userId, patientProfileId);
- return appointmentsRepo.findAppointmentsForPatient(patientProfile.id);
-}
-
-/**
- * Cross-module entry point for the consultations module: return the
- * appointment a doctor may act on, or null if the appointment doesn't exist
- * or isn't theirs. The consultations module never queries `Appointment`
- * directly β it calls this service instead (AGENTS.md hard rule 4).
- */
-export async function getAppointmentForDoctor(doctorUserId: string, appointmentId: string) {
- const appointment = await appointmentsRepo.findAppointmentById(prisma, appointmentId);
- if (!appointment) return null;
-
- const doctorProfile = await doctorsService.getDoctorProfileById(appointment.doctorProfileId);
- if (doctorProfile.userId !== doctorUserId) return null;
-
- return appointment;
-}
-
-/** Doctor's own scheduled appointments whose booked slot falls on the
- * current server-local calendar day. */
-export async function listDoctorAppointmentsToday(userId: string) {
- const doctor = await doctorsService.getMyDoctorProfile(userId);
- const now = new Date();
- const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
- const end = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
- return appointmentsRepo.findAppointmentsForDoctorOnDay(doctor.id, start, end);
-}
-
-/** This module's contribution to the patient data export (issue #40).
- * Slot times and the doctor's name are fetched per page through the
- * availability and doctors services β never by joining their tables. */
-export const appointmentsExportSection: patientsService.PatientExportSection = {
- name: 'appointments',
- version: 1,
- pages(subject, pageSize) {
- return patientsService.paginate(
- pageSize,
- (cursor, take) => appointmentsRepo.findAppointmentsPageForPatient(subject.patientProfileId, cursor, take),
- async (rows) => {
- // `availabilitySlotId` is nullable (Phase 3): a CANCELLED
- // appointment releases its slot back to the calendar, so its row
- // keeps its history but no longer points at one. Those rows export
- // with a null slot rather than a stale one.
- const slotIds = rows.map((row) => row.availabilitySlotId).filter((id): id is string => id !== null);
- const [slots, doctors] = await Promise.all([
- availabilityService.getSlotTimesByIds(slotIds),
- doctorsService.getDoctorSummariesByIds(rows.map((row) => row.doctorProfileId)),
- ]);
- return rows.map((row) => ({
- id: row.id,
- status: row.status,
- origin: row.origin,
- slot: row.availabilitySlotId ? (slots.get(row.availabilitySlotId) ?? null) : null,
- doctor: doctors.get(row.doctorProfileId) ?? null,
- createdAt: row.createdAt,
- updatedAt: row.updatedAt,
- }));
- },
- );
- },
-};
-
-/** Resolves an appointment for a doctor-owned consultation without exposing
- * the appointments repository across the module boundary. */
-export async function getDoctorAppointmentContext(userId: string, appointmentId: string) {
- const appointment = await appointmentsRepo.findAppointmentDoctorContext(appointmentId);
- if (!appointment) throw new HttpError(404, 'Appointment not found.');
- if (appointment.doctorProfile.userId !== userId) throw new HttpError(403, 'You can only issue orders for your own appointments.');
- if (appointment.doctorProfile.verificationStatus !== 'verified') throw new HttpError(403, 'A verified doctor profile is required to issue orders.');
- return { id: appointment.id, patientProfileId: appointment.patientProfileId, doctorProfileId: appointment.doctorProfileId };
-}
-
-/**
- * Creates the pre-populated appointment behind an accepted referral
- * (PRD FR-8 / issue #7). Called by the referrals service from inside its
- * accept transaction β same shape as bookSlot: conditional slot claim,
- * then the Appointment row, with `origin: 'referral'` and the referralId
- * link the cascade rules (issue #67) later react to. Appointment creation
- * belongs to this module; the referral state machine belongs to referrals.
- */
-export async function bookForReferral(
- db: Prisma.TransactionClient,
- input: {
- patientProfileId: string;
- doctorProfileId: string;
- availabilitySlotId: string;
- referralId: string;
- },
- actor: auditService.AuditActor,
-) {
- const claimed = await availabilityService.claimOpenSlot(db, input.availabilitySlotId);
- if (!claimed) {
- throw new HttpError(409, 'This slot is no longer available.');
- }
-
- const slot = await availabilityService.getSlot(db, input.availabilitySlotId);
- if (!slot) {
- throw new HttpError(404, 'Slot not found.');
- }
- if (slot.doctorProfileId !== input.doctorProfileId) {
- throw new HttpError(409, 'That slot does not belong to the referralβs target doctor.');
- }
-
- const appointment = await appointmentsRepo.createAppointment(db, {
- patientProfileId: input.patientProfileId,
- doctorProfileId: input.doctorProfileId,
- availabilitySlotId: input.availabilitySlotId,
- origin: 'referral',
- referralId: input.referralId,
- });
-
- // Same audit event a direct booking writes, in the same transaction β
- // a referral-origin appointment is still an appointment, and the event's
- // `origin` metadata is what tells the two apart. Without this, the
- // referral accept path would be the one way to create an appointment
- // with no trail at all.
- await auditService.record(db, actor, {
- entityType: 'Appointment',
- entityId: appointment.id,
- action: 'appointment.booked',
- fromState: null,
- toState: appointment.status,
- metadata: {
- availabilitySlotId: input.availabilitySlotId,
- doctorProfileId: input.doctorProfileId,
- origin: appointment.origin,
- referralId: input.referralId,
- },
- });
-
- return appointment;
-}
-
-/**
- * Moves a scheduled appointment to a terminal status, in one transaction:
- * the conditional status UPDATE, an optional slot release on cancellation,
- * and β if the appointment came from a referral β the cascade reaction in
- * the referrals module (issue #67, PRD Β§8.2.3). Running the transition
- * twice is impossible by construction: the conditional UPDATE only ever
- * matches a still-`scheduled` row, so the cascade hook below can only fire
- * once per real transition (and the cascade itself is additionally
- * conditional β see referralsService.applyAppointmentOutcome).
- */
-async function applyScheduledTransition(
- tx: Prisma.TransactionClient,
- appointment: { id: string; availabilitySlotId: string | null; referralId: string | null },
- newStatus: AppointmentStatus,
-) {
- const updated = await appointmentsRepo.updateStatusIfScheduled(tx, appointment.id, newStatus);
- if (updated.count !== 1) {
- throw new HttpError(409, 'This appointment is no longer scheduled.');
- }
-
- // Only cancellations free the calendar slot, and only while it's still
- // ahead of us β completed/no_show visits consumed the time either way.
- // The appointment row must then drop its unique slot link so the freed
- // slot can actually be re-booked.
- if (newStatus === 'cancelled' && appointment.availabilitySlotId) {
- const released = await availabilityService.releaseBookedSlotIfFuture(tx, appointment.availabilitySlotId);
- if (released) {
- await appointmentsRepo.clearAvailabilitySlotLink(tx, appointment.id);
- }
- }
-
- if (appointment.referralId) {
- await referralsService.applyAppointmentOutcome(tx, {
- appointmentId: appointment.id,
- referralId: appointment.referralId,
- status: newStatus as 'completed' | 'cancelled' | 'no_show',
- });
- }
-}
-
-/**
- * Doctor-side lifecycle transition (issue #3): the treating doctor marks
- * their own appointment completed / cancelled / no_show. Restricted to
- * the doctor the appointment is against β no admin-on-behalf path in v1.
- */
-export async function updateStatusByDoctor(userId: string, appointmentId: string, newStatus: AppointmentStatus) {
- const appointment = await appointmentsRepo.findAppointmentById(prisma, appointmentId);
- if (!appointment) throw new HttpError(404, 'Appointment not found.');
- await doctorsService.assertOwnsDoctorProfile(userId, appointment.doctorProfileId);
- if (transitionAppointmentStatus(appointment.status, newStatus) === null) {
- throw new HttpError(409, 'Only a scheduled appointment can change status.');
- }
-
- return prisma.$transaction(async (tx) => {
- await applyScheduledTransition(tx, appointment, newStatus);
- return appointmentsRepo.findAppointmentById(tx, appointmentId);
- });
-}
-
-/**
- * Patient-side cancellation of the patient's own scheduled appointment β
- * the "patient cancels" half of PRD Β§8.2.3. Deliberately narrower than
- * the doctor endpoint: a patient can only ever cancel (never mark
- * completed/no_show β those are clinical judgements for the doctor), and
- * only from `scheduled`.
- */
-export async function cancelByPatient(userId: string, appointmentId: string) {
- const patientProfile = await patientsService.getMyPatientProfile(userId);
- const appointment = await appointmentsRepo.findAppointmentById(prisma, appointmentId);
- if (!appointment) throw new HttpError(404, 'Appointment not found.');
- if (appointment.patientProfileId !== patientProfile.id) {
- throw new HttpError(403, 'You can only cancel your own appointments.');
- }
- if (appointment.status !== 'scheduled') {
- throw new HttpError(409, 'Only a scheduled appointment can be cancelled.');
- }
-
- return prisma.$transaction(async (tx) => {
- await applyScheduledTransition(tx, appointment, 'cancelled');
- return appointmentsRepo.findAppointmentById(tx, appointmentId);
- });
-}
-
-/**
- * Called by the referrals module's admin cancel (#68) inside its
- * transaction: close a live appointment without firing the referral
- * cascade back (the caller already knows it's cancelling the referral).
- * Appointment rows are still only ever written by this module β the
- * boundary holds.
- */
-export async function forceCancelAppointment(db: Prisma.TransactionClient, appointmentId: string) {
- const appointment = await appointmentsRepo.findAppointmentById(db, appointmentId);
- if (!appointment || appointment.status !== 'scheduled') {
- return null; // already terminal β idempotent for the caller
- }
- const updated = await appointmentsRepo.updateStatusIfScheduled(db, appointmentId, 'cancelled');
- if (updated.count === 1 && appointment.availabilitySlotId) {
- const released = await availabilityService.releaseBookedSlotIfFuture(db, appointment.availabilitySlotId);
- if (released) {
- await appointmentsRepo.clearAvailabilitySlotLink(db, appointmentId);
- }
- }
- return appointmentsRepo.findAppointmentById(db, appointmentId);
-}
-
-/** Retention hold (issue #41, docs/adr/0010): appointments are clinical
- * records, so a patient who has any is never purged after deletion. */
-export function hasAppointmentsForPatient(patientProfileId: string): Promise {
- return appointmentsRepo.patientHasAppointments(patientProfileId);
-}
+import { prisma } from '../../../db/prisma-client';
+import { HttpError } from '../../../http-error';
+import type { AppointmentStatus, Prisma } from '@prisma/client';
+import { transitionAppointmentStatus } from '../state-machine';
+import * as appointmentsRepo from '../repositories';
+import * as availabilityService from '../../availability/services';
+import * as doctorsService from '../../doctors/services';
+import * as patientsService from '../../patients/services';
+import * as auditService from '../../audit/services';
+import * as referralsService from '../../referrals/services';
+
+/**
+ * Books a slot for the calling patient. This is the correctness-critical
+ * path in the whole backend (ARCHITECTURE.md Β§5.1 / ROADMAP.md Phase 1 β
+ * "the single highest-priority correctness item in the whole roadmap").
+ *
+ * Two steps, one transaction:
+ * 1. `availabilityService.claimOpenSlot` β a conditional `UPDATE ...
+ * WHERE status = 'open'`, atomic at the DB level. Exactly one
+ * concurrent caller can ever see `count === 1` for a given slot.
+ * 2. Create the `Appointment` row.
+ * Both inside the same transaction so a failure between them can never
+ * leave a slot marked `booked` with no `Appointment` to show for it.
+ *
+ * See backend/tests/integration/booking.test.ts for the concurrency proof
+ * this is built to satisfy.
+ *
+ * Phase 8, issue #42 (docs/adr/0011): `patientProfileId` books for a
+ * dependent the caller is an active guardian of. Authorisation happens
+ * here, BEFORE the transaction β it only decides which profile the
+ * appointment is for. The transaction below is unchanged by it.
+ *
+ * Phase 8, issue #43: the booking's AuditEvent is written as a third step
+ * of the same transaction, after the Appointment row exists β so an event
+ * exists if and only if the booking committed. A caller that loses the
+ * race fails at step 1 and never reaches it. For a guardian booking, the
+ * actor is the guardian and the event's metadata names the dependent's
+ * profile id.
+ */
+export async function bookSlot(actor: auditService.AuditActor, availabilitySlotId: string, patientProfileId?: string) {
+ if (!actor.userId) throw new HttpError(401, 'Authentication required.');
+ const patientProfile = await patientsService.resolveActingProfile(actor.userId, patientProfileId);
+
+ return prisma.$transaction(async (tx) => {
+ const claimed = await availabilityService.claimOpenSlot(tx, availabilitySlotId);
+ if (!claimed) {
+ throw new HttpError(409, 'This slot is no longer available.');
+ }
+
+ const slot = await availabilityService.getSlot(tx, availabilitySlotId);
+ if (!slot) {
+ // Can't happen in practice (claimOpenSlot just matched this id), but
+ // keeps TypeScript honest and fails loudly instead of silently.
+ throw new HttpError(404, 'Slot not found.');
+ }
+
+ const appointment = await appointmentsRepo.createAppointment(tx, {
+ patientProfileId: patientProfile.id,
+ doctorProfileId: slot.doctorProfileId,
+ availabilitySlotId,
+ });
+
+ await auditService.record(tx, actor, {
+ entityType: 'Appointment',
+ entityId: appointment.id,
+ action: 'appointment.booked',
+ fromState: null,
+ toState: appointment.status,
+ metadata: {
+ availabilitySlotId,
+ doctorProfileId: slot.doctorProfileId,
+ patientProfileId: patientProfile.id,
+ origin: appointment.origin,
+ },
+ });
+
+ return appointment;
+ });
+}
+
+export async function listMyAppointments(userId: string, patientProfileId?: string) {
+ const patientProfile = await patientsService.resolveActingProfile(userId, patientProfileId);
+ return appointmentsRepo.findAppointmentsForPatient(patientProfile.id);
+}
+
+/**
+ * Cross-module entry point for the consultations module: return the
+ * appointment a doctor may act on, or null if the appointment doesn't exist
+ * or isn't theirs. The consultations module never queries `Appointment`
+ * directly β it calls this service instead (AGENTS.md hard rule 4).
+ */
+export async function getAppointmentForDoctor(doctorUserId: string, appointmentId: string) {
+ const appointment = await appointmentsRepo.findAppointmentById(prisma, appointmentId);
+ if (!appointment) return null;
+
+ const doctorProfile = await doctorsService.getDoctorProfileById(appointment.doctorProfileId);
+ if (doctorProfile.userId !== doctorUserId) return null;
+
+ return appointment;
+}
+
+/** Doctor's own scheduled appointments whose booked slot falls on the
+ * current server-local calendar day. */
+export async function listDoctorAppointmentsToday(userId: string) {
+ const doctor = await doctorsService.getMyDoctorProfile(userId);
+ const now = new Date();
+ const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+ const end = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
+ return appointmentsRepo.findAppointmentsForDoctorOnDay(doctor.id, start, end);
+}
+
+/** This module's contribution to the patient data export (issue #40).
+ * Slot times and the doctor's name are fetched per page through the
+ * availability and doctors services β never by joining their tables. */
+export const appointmentsExportSection: patientsService.PatientExportSection = {
+ name: 'appointments',
+ version: 1,
+ pages(subject, pageSize) {
+ return patientsService.paginate(
+ pageSize,
+ (cursor, take) => appointmentsRepo.findAppointmentsPageForPatient(subject.patientProfileId, cursor, take),
+ async (rows) => {
+ // `availabilitySlotId` is nullable (Phase 3): a CANCELLED
+ // appointment releases its slot back to the calendar, so its row
+ // keeps its history but no longer points at one. Those rows export
+ // with a null slot rather than a stale one.
+ const slotIds = rows.map((row) => row.availabilitySlotId).filter((id): id is string => id !== null);
+ const [slots, doctors] = await Promise.all([
+ availabilityService.getSlotTimesByIds(slotIds),
+ doctorsService.getDoctorSummariesByIds(rows.map((row) => row.doctorProfileId)),
+ ]);
+ return rows.map((row) => ({
+ id: row.id,
+ status: row.status,
+ origin: row.origin,
+ slot: row.availabilitySlotId ? (slots.get(row.availabilitySlotId) ?? null) : null,
+ doctor: doctors.get(row.doctorProfileId) ?? null,
+ createdAt: row.createdAt,
+ updatedAt: row.updatedAt,
+ }));
+ },
+ );
+ },
+};
+
+/** Resolves an appointment for a doctor-owned consultation without exposing
+ * the appointments repository across the module boundary. */
+export async function getDoctorAppointmentContext(userId: string, appointmentId: string) {
+ const appointment = await appointmentsRepo.findAppointmentDoctorContext(appointmentId);
+ if (!appointment) throw new HttpError(404, 'Appointment not found.');
+ if (appointment.doctorProfile.userId !== userId) throw new HttpError(403, 'You can only issue orders for your own appointments.');
+ if (appointment.doctorProfile.verificationStatus !== 'verified') throw new HttpError(403, 'A verified doctor profile is required to issue orders.');
+ return { id: appointment.id, patientProfileId: appointment.patientProfileId, doctorProfileId: appointment.doctorProfileId };
+}
+
+/**
+ * Creates the pre-populated appointment behind an accepted referral
+ * (PRD FR-8 / issue #7). Called by the referrals service from inside its
+ * accept transaction β same shape as bookSlot: conditional slot claim,
+ * then the Appointment row, with `origin: 'referral'` and the referralId
+ * link the cascade rules (issue #67) later react to. Appointment creation
+ * belongs to this module; the referral state machine belongs to referrals.
+ */
+export async function bookForReferral(
+ db: Prisma.TransactionClient,
+ input: {
+ patientProfileId: string;
+ doctorProfileId: string;
+ availabilitySlotId: string;
+ referralId: string;
+ },
+ actor: auditService.AuditActor,
+) {
+ const claimed = await availabilityService.claimOpenSlot(db, input.availabilitySlotId);
+ if (!claimed) {
+ throw new HttpError(409, 'This slot is no longer available.');
+ }
+
+ const slot = await availabilityService.getSlot(db, input.availabilitySlotId);
+ if (!slot) {
+ throw new HttpError(404, 'Slot not found.');
+ }
+ if (slot.doctorProfileId !== input.doctorProfileId) {
+ throw new HttpError(409, 'That slot does not belong to the referralβs target doctor.');
+ }
+
+ const appointment = await appointmentsRepo.createAppointment(db, {
+ patientProfileId: input.patientProfileId,
+ doctorProfileId: input.doctorProfileId,
+ availabilitySlotId: input.availabilitySlotId,
+ origin: 'referral',
+ referralId: input.referralId,
+ });
+
+ // Same audit event a direct booking writes, in the same transaction β
+ // a referral-origin appointment is still an appointment, and the event's
+ // `origin` metadata is what tells the two apart. Without this, the
+ // referral accept path would be the one way to create an appointment
+ // with no trail at all.
+ await auditService.record(db, actor, {
+ entityType: 'Appointment',
+ entityId: appointment.id,
+ action: 'appointment.booked',
+ fromState: null,
+ toState: appointment.status,
+ metadata: {
+ availabilitySlotId: input.availabilitySlotId,
+ doctorProfileId: input.doctorProfileId,
+ origin: appointment.origin,
+ referralId: input.referralId,
+ },
+ });
+
+ return appointment;
+}
+
+/**
+ * Moves a scheduled appointment to a terminal status, in one transaction:
+ * the conditional status UPDATE, an optional slot release on cancellation,
+ * and β if the appointment came from a referral β the cascade reaction in
+ * the referrals module (issue #67, PRD Β§8.2.3). Running the transition
+ * twice is impossible by construction: the conditional UPDATE only ever
+ * matches a still-`scheduled` row, so the cascade hook below can only fire
+ * once per real transition (and the cascade itself is additionally
+ * conditional β see referralsService.applyAppointmentOutcome).
+ */
+async function applyScheduledTransition(
+ tx: Prisma.TransactionClient,
+ appointment: { id: string; availabilitySlotId: string | null; referralId: string | null },
+ newStatus: AppointmentStatus,
+) {
+ const updated = await appointmentsRepo.updateStatusIfScheduled(tx, appointment.id, newStatus);
+ if (updated.count !== 1) {
+ throw new HttpError(409, 'This appointment is no longer scheduled.');
+ }
+
+ // Only cancellations free the calendar slot, and only while it's still
+ // ahead of us β completed/no_show visits consumed the time either way.
+ // The appointment row must then drop its unique slot link so the freed
+ // slot can actually be re-booked.
+ if (newStatus === 'cancelled' && appointment.availabilitySlotId) {
+ const released = await availabilityService.releaseBookedSlotIfFuture(tx, appointment.availabilitySlotId);
+ if (released) {
+ await appointmentsRepo.clearAvailabilitySlotLink(tx, appointment.id);
+ }
+ }
+
+ if (appointment.referralId) {
+ await referralsService.applyAppointmentOutcome(tx, {
+ appointmentId: appointment.id,
+ referralId: appointment.referralId,
+ status: newStatus as 'completed' | 'cancelled' | 'no_show',
+ });
+ }
+}
+
+/**
+ * Doctor-side lifecycle transition (issue #3): the treating doctor marks
+ * their own appointment completed / cancelled / no_show. Restricted to
+ * the doctor the appointment is against β no admin-on-behalf path in v1.
+ */
+export async function updateStatusByDoctor(userId: string, appointmentId: string, newStatus: AppointmentStatus) {
+ const appointment = await appointmentsRepo.findAppointmentById(prisma, appointmentId);
+ if (!appointment) throw new HttpError(404, 'Appointment not found.');
+ await doctorsService.assertOwnsDoctorProfile(userId, appointment.doctorProfileId);
+ if (transitionAppointmentStatus(appointment.status, newStatus) === null) {
+ throw new HttpError(409, 'Only a scheduled appointment can change status.');
+ }
+
+ return prisma.$transaction(async (tx) => {
+ await applyScheduledTransition(tx, appointment, newStatus);
+ return appointmentsRepo.findAppointmentById(tx, appointmentId);
+ });
+}
+
+/**
+ * Patient-side cancellation of the patient's own scheduled appointment β
+ * the "patient cancels" half of PRD Β§8.2.3. Deliberately narrower than
+ * the doctor endpoint: a patient can only ever cancel (never mark
+ * completed/no_show β those are clinical judgements for the doctor), and
+ * only from `scheduled`.
+ */
+export async function cancelByPatient(userId: string, appointmentId: string) {
+ const patientProfile = await patientsService.getMyPatientProfile(userId);
+ const appointment = await appointmentsRepo.findAppointmentById(prisma, appointmentId);
+ if (!appointment) throw new HttpError(404, 'Appointment not found.');
+ if (appointment.patientProfileId !== patientProfile.id) {
+ throw new HttpError(403, 'You can only cancel your own appointments.');
+ }
+ if (appointment.status !== 'scheduled') {
+ throw new HttpError(409, 'Only a scheduled appointment can be cancelled.');
+ }
+
+ return prisma.$transaction(async (tx) => {
+ await applyScheduledTransition(tx, appointment, 'cancelled');
+ return appointmentsRepo.findAppointmentById(tx, appointmentId);
+ });
+}
+
+/**
+ * Called by the referrals module's admin cancel (#68) inside its
+ * transaction: close a live appointment without firing the referral
+ * cascade back (the caller already knows it's cancelling the referral).
+ * Appointment rows are still only ever written by this module β the
+ * boundary holds.
+ */
+export async function forceCancelAppointment(db: Prisma.TransactionClient, appointmentId: string) {
+ const appointment = await appointmentsRepo.findAppointmentById(db, appointmentId);
+ if (!appointment || appointment.status !== 'scheduled') {
+ return null; // already terminal β idempotent for the caller
+ }
+ const updated = await appointmentsRepo.updateStatusIfScheduled(db, appointmentId, 'cancelled');
+ if (updated.count === 1 && appointment.availabilitySlotId) {
+ const released = await availabilityService.releaseBookedSlotIfFuture(db, appointment.availabilitySlotId);
+ if (released) {
+ await appointmentsRepo.clearAvailabilitySlotLink(db, appointmentId);
+ }
+ }
+ return appointmentsRepo.findAppointmentById(db, appointmentId);
+}
+
+/** Retention hold (issue #41, docs/adr/0010): appointments are clinical
+ * records, so a patient who has any is never purged after deletion. */
+export function hasAppointmentsForPatient(patientProfileId: string): Promise {
+ return appointmentsRepo.patientHasAppointments(patientProfileId);
+}
diff --git a/backend/src/modules/appointments/state-machine.ts b/backend/src/modules/appointments/state-machine.ts
index a75d9029..4be271c4 100644
--- a/backend/src/modules/appointments/state-machine.ts
+++ b/backend/src/modules/appointments/state-machine.ts
@@ -1,16 +1,16 @@
-import type { AppointmentStatus } from '@prisma/client';
-
-const transitions: Record = {
- scheduled: ['completed', 'cancelled', 'no_show'],
- completed: [],
- cancelled: [],
- no_show: [],
-};
-
-/** Returns the next state or null when a requested lifecycle transition is illegal. */
-export function transitionAppointmentStatus(
- current: AppointmentStatus,
- requested: AppointmentStatus,
-): AppointmentStatus | null {
- return transitions[current].includes(requested) ? requested : null;
-}
+import type { AppointmentStatus } from '@prisma/client';
+
+const transitions: Record = {
+ scheduled: ['completed', 'cancelled', 'no_show'],
+ completed: [],
+ cancelled: [],
+ no_show: [],
+};
+
+/** Returns the next state or null when a requested lifecycle transition is illegal. */
+export function transitionAppointmentStatus(
+ current: AppointmentStatus,
+ requested: AppointmentStatus,
+): AppointmentStatus | null {
+ return transitions[current].includes(requested) ? requested : null;
+}
diff --git a/backend/src/modules/appointments/validation.ts b/backend/src/modules/appointments/validation.ts
index 8a730943..eaf74d2a 100644
--- a/backend/src/modules/appointments/validation.ts
+++ b/backend/src/modules/appointments/validation.ts
@@ -1,18 +1,18 @@
-import { z } from 'zod';
-
-export const bookSlotSchema = z.object({
- availabilitySlotId: z.string().min(1),
- // Phase 8, issue #42: book for a dependent the caller is guardian of.
- // Omitted (or the caller's own profile id) = book for yourself.
- patientProfileId: z.string().min(1).optional(),
-});
-
-// Phase 8, issue #42: `GET /appointments/mine?patientProfileId=` lists a
-// dependent's appointments, authorised the same way as booking.
-export const listMineQuerySchema = z.object({
- patientProfileId: z.string().min(1).optional(),
-});
-
-export const updateStatusSchema = z.object({
- status: z.enum(['completed', 'cancelled', 'no_show']),
-});
+import { z } from 'zod';
+
+export const bookSlotSchema = z.object({
+ availabilitySlotId: z.string().min(1),
+ // Phase 8, issue #42: book for a dependent the caller is guardian of.
+ // Omitted (or the caller's own profile id) = book for yourself.
+ patientProfileId: z.string().min(1).optional(),
+});
+
+// Phase 8, issue #42: `GET /appointments/mine?patientProfileId=` lists a
+// dependent's appointments, authorised the same way as booking.
+export const listMineQuerySchema = z.object({
+ patientProfileId: z.string().min(1).optional(),
+});
+
+export const updateStatusSchema = z.object({
+ status: z.enum(['completed', 'cancelled', 'no_show']),
+});
diff --git a/backend/src/modules/audit/services/index.ts b/backend/src/modules/audit/services/index.ts
index 16f28691..feebc49f 100644
--- a/backend/src/modules/audit/services/index.ts
+++ b/backend/src/modules/audit/services/index.ts
@@ -1,315 +1,315 @@
-// audit services (Phase 8, issue #43) β the write helper every module calls
-// when it changes the state of an accountable record, plus the admin query.
-// Other modules import from here only, never from audit/repositories.
-//
-// The contract of record():
-// - it runs INSIDE the transaction of the change it describes (it refuses
-// the root client), so an event exists if and only if its change
-// committed;
-// - it carries only what's needed to attribute the transition β actor,
-// entity, action, from/to state, request id β plus a small, flat,
-// validated metadata map of ids/codes. Never PHI, never request bodies.
-import type { Prisma, Role } from '@prisma/client';
-import { config } from '../../../config';
-import { HttpError } from '../../../http-error';
-import * as auditRepo from '../repositories';
-
-/** Who caused a transition. Controllers build it from the request with
- * `auditActor(req)` (src/middleware/request-id.ts). `userId`/`role` are null
- * only for system-initiated changes. */
-export interface AuditActor {
- userId: string | null;
- role: Role | null;
- requestId: string | null;
-}
-
-export const SYSTEM_ACTOR: AuditActor = { userId: null, role: null, requestId: null };
-
-export type AuditEntityType =
- | 'Appointment'
- | 'DoctorProfile'
- | 'PaymentIntent'
- | 'Consultation'
- | 'Prescription'
- | 'LabOrder'
- | 'Referral'
- | 'FacilityPrice'
- // Issue #14: admin-only facility account creation (`facility.created`).
- | 'FacilityProfile'
- | 'Order'
- | 'OrderDispute';
-
-export type AuditMetadataValue = string | number | boolean | null;
-export type AuditMetadata = Record;
-
-export interface AuditEventInput {
- entityType: AuditEntityType;
- entityId: string;
- action: string;
- fromState: string | null;
- toState: string | null;
- metadata?: AuditMetadata;
-}
-
-const ACTION = /^[a-z][a-z_]*(\.[a-z][a-z_]*)+$/;
-const METADATA_KEY = /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/;
-const MAX_METADATA_KEYS = 16;
-const MAX_METADATA_STRING = 200;
-/** Defence in depth against a caller passing personal or secret data: any
- * key that *looks* like it could carry it is refused outright. Callers
- * should only ever pass ids and codes. */
-const FORBIDDEN_METADATA_KEY =
- /(name|email|phone|dob|birth|address|note|intake|symptom|diagnos|password|secret|token|key|credential|body|payload)/i;
-
-export class AuditMetadataError extends Error {
- constructor(message: string) {
- super(message);
- this.name = 'AuditMetadataError';
- }
-}
-
-/** Validates metadata: flat scalars only, bounded, no suspicious keys.
- * Exported for tests. Throws AuditMetadataError β a programming error, so
- * it aborts the surrounding transaction rather than being swallowed. */
-export function validateAuditMetadata(metadata: AuditMetadata | undefined): AuditMetadata | undefined {
- if (metadata === undefined) return undefined;
- if (metadata === null || typeof metadata !== 'object' || Array.isArray(metadata)) {
- throw new AuditMetadataError('Audit metadata must be a flat object.');
- }
- const entries = Object.entries(metadata);
- if (entries.length > MAX_METADATA_KEYS) throw new AuditMetadataError('Audit metadata has too many keys.');
- for (const [key, value] of entries) {
- if (!METADATA_KEY.test(key)) throw new AuditMetadataError(`Invalid audit metadata key: ${key}`);
- if (FORBIDDEN_METADATA_KEY.test(key) && !/Id$/.test(key)) {
- throw new AuditMetadataError(`Audit metadata key not allowed (could carry personal or secret data): ${key}`);
- }
- const ok =
- value === null ||
- typeof value === 'boolean' ||
- (typeof value === 'number' && Number.isFinite(value)) ||
- (typeof value === 'string' && value.length <= MAX_METADATA_STRING);
- if (!ok) throw new AuditMetadataError(`Audit metadata value for ${key} must be a short scalar.`);
- }
- return metadata;
-}
-
-/**
- * Records one state transition, inside the caller's transaction. Passing
- * the root Prisma client (which would commit the event on its own, before
- * β or without β the change) is refused at runtime.
- *
- * The root client is recognised by its lifecycle methods ($connect /
- * $disconnect): they exist on the root client and never on the
- * interactive-transaction client. It used to be recognised by
- * `$transaction` instead, but Prisma 7 (the issue #47 upgrade) gives the
- * transaction client a `$transaction` method too β identical to the root
- * client's β so that check started refusing every *legitimate* in-transaction
- * call, aborting the change with it. Verified against prisma@7.10.1.
- */
-export async function record(tx: Prisma.TransactionClient, actor: AuditActor, event: AuditEventInput): Promise {
- const client = tx as unknown as { $connect?: unknown; $disconnect?: unknown };
- if (typeof client.$connect === 'function' || typeof client.$disconnect === 'function') {
- throw new Error('audit.record() must run inside the transaction of the change it describes.');
- }
- if (!ACTION.test(event.action)) throw new Error(`Invalid audit action: ${event.action}`);
- const metadata = validateAuditMetadata(event.metadata);
- await auditRepo.insertAuditEvent(tx, {
- actorUserId: actor.userId,
- actorRole: actor.role,
- entityType: event.entityType,
- entityId: event.entityId,
- action: event.action,
- fromState: event.fromState,
- toState: event.toState,
- requestId: actor.requestId,
- metadata,
- });
-}
-
-// --- Admin query -----------------------------------------------------------
-
-export interface AuditEventFilters {
- entityType?: string;
- entityId?: string;
- actorUserId?: string;
- from?: Date;
- to?: Date;
- cursor?: string;
- limit: number;
-}
-
-/** Opaque cursor: base64url of "|" of the last item. */
-export function encodeAuditCursor(event: { createdAt: Date; id: string }): string {
- return Buffer.from(`${event.createdAt.toISOString()}|${event.id}`, 'utf8').toString('base64url');
-}
-
-export function decodeAuditCursor(cursor: string): { createdAt: Date; id: string } {
- const raw = Buffer.from(cursor, 'base64url').toString('utf8');
- const separator = raw.indexOf('|');
- const createdAt = new Date(raw.slice(0, separator));
- const id = raw.slice(separator + 1);
- if (separator < 1 || Number.isNaN(createdAt.getTime()) || !id) {
- throw new HttpError(400, 'Invalid cursor.', 'INVALID_CURSOR');
- }
- return { createdAt, id };
-}
-
-export async function listAuditEvents(filters: AuditEventFilters) {
- if (filters.from && filters.to && filters.from.getTime() > filters.to.getTime()) {
- throw new HttpError(400, '`from` must not be after `to`.');
- }
- const rows = await auditRepo.findAuditEvents({
- entityType: filters.entityType,
- entityId: filters.entityId,
- actorUserId: filters.actorUserId,
- from: filters.from,
- to: filters.to,
- before: filters.cursor ? decodeAuditCursor(filters.cursor) : undefined,
- // One extra row tells us whether there's a next page without a count.
- take: filters.limit + 1,
- });
- const items = rows.slice(0, filters.limit);
- const last = items[items.length - 1];
- return { items, nextCursor: rows.length > filters.limit && last ? encodeAuditCursor(last) : null };
-}
-
-// --- PHI read-access logging (Phase 8, issue #45) ---------------------------
-// Answers "who looked at this" β distinct from record()'s "who changed this".
-// Writes are fire-and-forget (never awaited on the read path) and fail
-// open: a logging failure must never fail the read, but it IS counted and
-// surfaced for alerting via GET /admin/access-logs/stats.
-
-/** Entity kinds a read can be logged against. Mirrors the schema comment
- * on AccessLog.entityType β extend both together. */
-export type AccessEntityType =
- | 'IntakeForm'
- | 'Consultation'
- | 'Prescription'
- | 'PrescriptionItem'
- | 'LabOrder'
- | 'LabResult'
- | 'Referral'
- | 'PaymentIntent'
- | 'PatientProfile'
- | 'Appointment';
-
-export interface AccessLogInput {
- /** The viewer. Null only for anonymous/system reads (there are none today). */
- accessedBy?: string | null;
- /** The record owner (whose data was viewed) β used by the patient-facing
- * /patients/me/access-log. Null when the read isn't attributable to one. */
- patientProfileId?: string | null;
- entityType: AccessEntityType;
- entityId: string;
- route?: string | null;
- /** Context string β no PHI, just enough to correlate with request logs. */
- detail?: string | null;
-}
-
-/** Retention window for AccessLog rows (env ACCESS_LOG_RETENTION_DAYS).
- * pruneOldAccessLogs() deletes anything older; wired into the prune
- * interval in src/server.ts. */
-export const ACCESS_LOG_RETENTION_DAYS = config.accessLog.retentionDays;
-
-let logFailureCount = 0;
-
-/** Cumulative count of failed access-log writes since process start β
- * readable by admins via GET /admin/access-logs/stats for alerting. */
-export function getLogFailureCount(): number {
- return logFailureCount;
-}
-
-/** Records one read, off the call path: returns immediately, never throws.
- * Callers must NOT await this on a request path β the unit test asserting
- * sub-50ms return with a hanging insert is the guard. */
-export function recordAccess(input: AccessLogInput): void {
- void auditRepo
- .recordAccess({
- accessedBy: input.accessedBy ?? null,
- patientProfileId: input.patientProfileId ?? null,
- entityType: input.entityType,
- entityId: input.entityId,
- route: input.route ?? null,
- detail: input.detail ?? null,
- })
- .catch((err: unknown) => {
- logFailureCount += 1;
- // Fail-open by design, but loudly: this is a privacy trail, so a
- // silent gap is worse than a noisy one.
- console.error(`[audit] FAILED to record access (${input.entityType} ${input.entityId})`, err);
- });
-}
-
-// --- Patient-facing query (PII stripped) ------------------------------------
-
-export interface PatientAccessLogEntry {
- id: string;
- accessedAt: Date;
- entityType: string;
- entityId: string;
- /** The viewer's role only β never their email or internal id. A deleted
- * viewer (FK SetNull) surfaces as the 'deleted_user' placeholder. */
- viewerRole: string;
-}
-
-/** Who viewed MY records. The caller supplies only their own
- * patientProfileId (resolved server-side from the authenticated user, never
- * from the request), and the response carries no viewer PII. */
-export async function getAccessForPatient(
- patientProfileId: string,
- limit = 100,
-): Promise {
- const rows = await auditRepo.findAccessForPatient({ patientProfileId, take: limit });
- return rows.map((row) => ({
- id: row.id,
- accessedAt: row.accessedAt,
- entityType: row.entityType,
- entityId: row.entityId,
- viewerRole: row.accessedByUser?.role ?? 'deleted_user',
- }));
-}
-
-// --- Admin queries ----------------------------------------------------------
-
-/** Admin: who accessed a specific kind of record (optionally a specific
- * one). Unlike the patient-facing query this keeps viewer ids β admins
- * need attribution, and role-only would be useless for incident response. */
-export function listAccessForEntity(filters: { entityType: string; entityId?: string; limit?: number }) {
- return auditRepo.findAccessByEntity({
- entityType: filters.entityType,
- entityId: filters.entityId,
- take: filters.limit ?? 50,
- });
-}
-
-/** Admin: what a specific user accessed. */
-export function listAccessForUser(filters: { userId: string; limit?: number }) {
- return auditRepo.findAccessByUser({
- accessedBy: filters.userId,
- take: filters.limit ?? 50,
- });
-}
-
-/** Admin: cumulative write-failure count + retention config (alerting). */
-export function getAccessLogStats() {
- return { logFailureCount, retentionDays: ACCESS_LOG_RETENTION_DAYS };
-}
-
-// --- Retention --------------------------------------------------------------
-
-/** Deletes AccessLog rows older than the retention window. Returns the
- * number of rows removed. Runs from the prune interval in src/server.ts. */
-export async function pruneOldAccessLogs(): Promise {
- const cutoff = new Date(Date.now() - ACCESS_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000);
- return auditRepo.pruneAccessLogs(cutoff);
-}
-
-/** Retention hold (issue #41, docs/adr/0010): AccessLog.patientProfileId
- * cascades, so purging a profile would erase the record of who viewed it.
- * The purge waits until the access log's own retention prune has removed
- * those rows (ACCESS_LOG_RETENTION_DAYS). */
-export function hasAccessLogsForPatient(patientProfileId: string): Promise {
- // Reuses the existing read rather than adding a repository export: this
- // repository's surface is pinned (tests/integration/audit.test.mts).
- return auditRepo.findAccessForPatient({ patientProfileId, take: 1 }).then((rows) => rows.length > 0);
-}
+// audit services (Phase 8, issue #43) β the write helper every module calls
+// when it changes the state of an accountable record, plus the admin query.
+// Other modules import from here only, never from audit/repositories.
+//
+// The contract of record():
+// - it runs INSIDE the transaction of the change it describes (it refuses
+// the root client), so an event exists if and only if its change
+// committed;
+// - it carries only what's needed to attribute the transition β actor,
+// entity, action, from/to state, request id β plus a small, flat,
+// validated metadata map of ids/codes. Never PHI, never request bodies.
+import type { Prisma, Role } from '@prisma/client';
+import { config } from '../../../config';
+import { HttpError } from '../../../http-error';
+import * as auditRepo from '../repositories';
+
+/** Who caused a transition. Controllers build it from the request with
+ * `auditActor(req)` (src/middleware/request-id.ts). `userId`/`role` are null
+ * only for system-initiated changes. */
+export interface AuditActor {
+ userId: string | null;
+ role: Role | null;
+ requestId: string | null;
+}
+
+export const SYSTEM_ACTOR: AuditActor = { userId: null, role: null, requestId: null };
+
+export type AuditEntityType =
+ | 'Appointment'
+ | 'DoctorProfile'
+ | 'PaymentIntent'
+ | 'Consultation'
+ | 'Prescription'
+ | 'LabOrder'
+ | 'Referral'
+ | 'FacilityPrice'
+ // Issue #14: admin-only facility account creation (`facility.created`).
+ | 'FacilityProfile'
+ | 'Order'
+ | 'OrderDispute';
+
+export type AuditMetadataValue = string | number | boolean | null;
+export type AuditMetadata = Record;
+
+export interface AuditEventInput {
+ entityType: AuditEntityType;
+ entityId: string;
+ action: string;
+ fromState: string | null;
+ toState: string | null;
+ metadata?: AuditMetadata;
+}
+
+const ACTION = /^[a-z][a-z_]*(\.[a-z][a-z_]*)+$/;
+const METADATA_KEY = /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/;
+const MAX_METADATA_KEYS = 16;
+const MAX_METADATA_STRING = 200;
+/** Defence in depth against a caller passing personal or secret data: any
+ * key that *looks* like it could carry it is refused outright. Callers
+ * should only ever pass ids and codes. */
+const FORBIDDEN_METADATA_KEY =
+ /(name|email|phone|dob|birth|address|note|intake|symptom|diagnos|password|secret|token|key|credential|body|payload)/i;
+
+export class AuditMetadataError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'AuditMetadataError';
+ }
+}
+
+/** Validates metadata: flat scalars only, bounded, no suspicious keys.
+ * Exported for tests. Throws AuditMetadataError β a programming error, so
+ * it aborts the surrounding transaction rather than being swallowed. */
+export function validateAuditMetadata(metadata: AuditMetadata | undefined): AuditMetadata | undefined {
+ if (metadata === undefined) return undefined;
+ if (metadata === null || typeof metadata !== 'object' || Array.isArray(metadata)) {
+ throw new AuditMetadataError('Audit metadata must be a flat object.');
+ }
+ const entries = Object.entries(metadata);
+ if (entries.length > MAX_METADATA_KEYS) throw new AuditMetadataError('Audit metadata has too many keys.');
+ for (const [key, value] of entries) {
+ if (!METADATA_KEY.test(key)) throw new AuditMetadataError(`Invalid audit metadata key: ${key}`);
+ if (FORBIDDEN_METADATA_KEY.test(key) && !/Id$/.test(key)) {
+ throw new AuditMetadataError(`Audit metadata key not allowed (could carry personal or secret data): ${key}`);
+ }
+ const ok =
+ value === null ||
+ typeof value === 'boolean' ||
+ (typeof value === 'number' && Number.isFinite(value)) ||
+ (typeof value === 'string' && value.length <= MAX_METADATA_STRING);
+ if (!ok) throw new AuditMetadataError(`Audit metadata value for ${key} must be a short scalar.`);
+ }
+ return metadata;
+}
+
+/**
+ * Records one state transition, inside the caller's transaction. Passing
+ * the root Prisma client (which would commit the event on its own, before
+ * β or without β the change) is refused at runtime.
+ *
+ * The root client is recognised by its lifecycle methods ($connect /
+ * $disconnect): they exist on the root client and never on the
+ * interactive-transaction client. It used to be recognised by
+ * `$transaction` instead, but Prisma 7 (the issue #47 upgrade) gives the
+ * transaction client a `$transaction` method too β identical to the root
+ * client's β so that check started refusing every *legitimate* in-transaction
+ * call, aborting the change with it. Verified against prisma@7.10.1.
+ */
+export async function record(tx: Prisma.TransactionClient, actor: AuditActor, event: AuditEventInput): Promise {
+ const client = tx as unknown as { $connect?: unknown; $disconnect?: unknown };
+ if (typeof client.$connect === 'function' || typeof client.$disconnect === 'function') {
+ throw new Error('audit.record() must run inside the transaction of the change it describes.');
+ }
+ if (!ACTION.test(event.action)) throw new Error(`Invalid audit action: ${event.action}`);
+ const metadata = validateAuditMetadata(event.metadata);
+ await auditRepo.insertAuditEvent(tx, {
+ actorUserId: actor.userId,
+ actorRole: actor.role,
+ entityType: event.entityType,
+ entityId: event.entityId,
+ action: event.action,
+ fromState: event.fromState,
+ toState: event.toState,
+ requestId: actor.requestId,
+ metadata,
+ });
+}
+
+// --- Admin query -----------------------------------------------------------
+
+export interface AuditEventFilters {
+ entityType?: string;
+ entityId?: string;
+ actorUserId?: string;
+ from?: Date;
+ to?: Date;
+ cursor?: string;
+ limit: number;
+}
+
+/** Opaque cursor: base64url of "|" of the last item. */
+export function encodeAuditCursor(event: { createdAt: Date; id: string }): string {
+ return Buffer.from(`${event.createdAt.toISOString()}|${event.id}`, 'utf8').toString('base64url');
+}
+
+export function decodeAuditCursor(cursor: string): { createdAt: Date; id: string } {
+ const raw = Buffer.from(cursor, 'base64url').toString('utf8');
+ const separator = raw.indexOf('|');
+ const createdAt = new Date(raw.slice(0, separator));
+ const id = raw.slice(separator + 1);
+ if (separator < 1 || Number.isNaN(createdAt.getTime()) || !id) {
+ throw new HttpError(400, 'Invalid cursor.', 'INVALID_CURSOR');
+ }
+ return { createdAt, id };
+}
+
+export async function listAuditEvents(filters: AuditEventFilters) {
+ if (filters.from && filters.to && filters.from.getTime() > filters.to.getTime()) {
+ throw new HttpError(400, '`from` must not be after `to`.');
+ }
+ const rows = await auditRepo.findAuditEvents({
+ entityType: filters.entityType,
+ entityId: filters.entityId,
+ actorUserId: filters.actorUserId,
+ from: filters.from,
+ to: filters.to,
+ before: filters.cursor ? decodeAuditCursor(filters.cursor) : undefined,
+ // One extra row tells us whether there's a next page without a count.
+ take: filters.limit + 1,
+ });
+ const items = rows.slice(0, filters.limit);
+ const last = items[items.length - 1];
+ return { items, nextCursor: rows.length > filters.limit && last ? encodeAuditCursor(last) : null };
+}
+
+// --- PHI read-access logging (Phase 8, issue #45) ---------------------------
+// Answers "who looked at this" β distinct from record()'s "who changed this".
+// Writes are fire-and-forget (never awaited on the read path) and fail
+// open: a logging failure must never fail the read, but it IS counted and
+// surfaced for alerting via GET /admin/access-logs/stats.
+
+/** Entity kinds a read can be logged against. Mirrors the schema comment
+ * on AccessLog.entityType β extend both together. */
+export type AccessEntityType =
+ | 'IntakeForm'
+ | 'Consultation'
+ | 'Prescription'
+ | 'PrescriptionItem'
+ | 'LabOrder'
+ | 'LabResult'
+ | 'Referral'
+ | 'PaymentIntent'
+ | 'PatientProfile'
+ | 'Appointment';
+
+export interface AccessLogInput {
+ /** The viewer. Null only for anonymous/system reads (there are none today). */
+ accessedBy?: string | null;
+ /** The record owner (whose data was viewed) β used by the patient-facing
+ * /patients/me/access-log. Null when the read isn't attributable to one. */
+ patientProfileId?: string | null;
+ entityType: AccessEntityType;
+ entityId: string;
+ route?: string | null;
+ /** Context string β no PHI, just enough to correlate with request logs. */
+ detail?: string | null;
+}
+
+/** Retention window for AccessLog rows (env ACCESS_LOG_RETENTION_DAYS).
+ * pruneOldAccessLogs() deletes anything older; wired into the prune
+ * interval in src/server.ts. */
+export const ACCESS_LOG_RETENTION_DAYS = config.accessLog.retentionDays;
+
+let logFailureCount = 0;
+
+/** Cumulative count of failed access-log writes since process start β
+ * readable by admins via GET /admin/access-logs/stats for alerting. */
+export function getLogFailureCount(): number {
+ return logFailureCount;
+}
+
+/** Records one read, off the call path: returns immediately, never throws.
+ * Callers must NOT await this on a request path β the unit test asserting
+ * sub-50ms return with a hanging insert is the guard. */
+export function recordAccess(input: AccessLogInput): void {
+ void auditRepo
+ .recordAccess({
+ accessedBy: input.accessedBy ?? null,
+ patientProfileId: input.patientProfileId ?? null,
+ entityType: input.entityType,
+ entityId: input.entityId,
+ route: input.route ?? null,
+ detail: input.detail ?? null,
+ })
+ .catch((err: unknown) => {
+ logFailureCount += 1;
+ // Fail-open by design, but loudly: this is a privacy trail, so a
+ // silent gap is worse than a noisy one.
+ console.error(`[audit] FAILED to record access (${input.entityType} ${input.entityId})`, err);
+ });
+}
+
+// --- Patient-facing query (PII stripped) ------------------------------------
+
+export interface PatientAccessLogEntry {
+ id: string;
+ accessedAt: Date;
+ entityType: string;
+ entityId: string;
+ /** The viewer's role only β never their email or internal id. A deleted
+ * viewer (FK SetNull) surfaces as the 'deleted_user' placeholder. */
+ viewerRole: string;
+}
+
+/** Who viewed MY records. The caller supplies only their own
+ * patientProfileId (resolved server-side from the authenticated user, never
+ * from the request), and the response carries no viewer PII. */
+export async function getAccessForPatient(
+ patientProfileId: string,
+ limit = 100,
+): Promise {
+ const rows = await auditRepo.findAccessForPatient({ patientProfileId, take: limit });
+ return rows.map((row) => ({
+ id: row.id,
+ accessedAt: row.accessedAt,
+ entityType: row.entityType,
+ entityId: row.entityId,
+ viewerRole: row.accessedByUser?.role ?? 'deleted_user',
+ }));
+}
+
+// --- Admin queries ----------------------------------------------------------
+
+/** Admin: who accessed a specific kind of record (optionally a specific
+ * one). Unlike the patient-facing query this keeps viewer ids β admins
+ * need attribution, and role-only would be useless for incident response. */
+export function listAccessForEntity(filters: { entityType: string; entityId?: string; limit?: number }) {
+ return auditRepo.findAccessByEntity({
+ entityType: filters.entityType,
+ entityId: filters.entityId,
+ take: filters.limit ?? 50,
+ });
+}
+
+/** Admin: what a specific user accessed. */
+export function listAccessForUser(filters: { userId: string; limit?: number }) {
+ return auditRepo.findAccessByUser({
+ accessedBy: filters.userId,
+ take: filters.limit ?? 50,
+ });
+}
+
+/** Admin: cumulative write-failure count + retention config (alerting). */
+export function getAccessLogStats() {
+ return { logFailureCount, retentionDays: ACCESS_LOG_RETENTION_DAYS };
+}
+
+// --- Retention --------------------------------------------------------------
+
+/** Deletes AccessLog rows older than the retention window. Returns the
+ * number of rows removed. Runs from the prune interval in src/server.ts. */
+export async function pruneOldAccessLogs(): Promise {
+ const cutoff = new Date(Date.now() - ACCESS_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000);
+ return auditRepo.pruneAccessLogs(cutoff);
+}
+
+/** Retention hold (issue #41, docs/adr/0010): AccessLog.patientProfileId
+ * cascades, so purging a profile would erase the record of who viewed it.
+ * The purge waits until the access log's own retention prune has removed
+ * those rows (ACCESS_LOG_RETENTION_DAYS). */
+export function hasAccessLogsForPatient(patientProfileId: string): Promise {
+ // Reuses the existing read rather than adding a repository export: this
+ // repository's surface is pinned (tests/integration/audit.test.mts).
+ return auditRepo.findAccessForPatient({ patientProfileId, take: 1 }).then((rows) => rows.length > 0);
+}
diff --git a/backend/src/modules/auth/controllers/index.ts b/backend/src/modules/auth/controllers/index.ts
index c444e5d6..67b7314a 100644
--- a/backend/src/modules/auth/controllers/index.ts
+++ b/backend/src/modules/auth/controllers/index.ts
@@ -1,227 +1,227 @@
-import type { Request, Response } from 'express';
-import * as authService from '../services';
-import {
- clearLoginAttemptsSchema,
- loginSchema,
- passwordResetConfirmSchema,
- passwordResetRequestSchema,
- signupSchema,
- signupWithInviteSchema,
- verifyEmailSchema,
-} from '../validation';
-import { HttpError } from '../../../http-error';
-import { config } from '../../../config';
-import { readCsrfHeader } from '../../../middleware/csrf';
-import { getParam } from '../../../request-params';
-import { resolveLocale } from '../../../i18n';
-
-const REFRESH_COOKIE = 'docforum_refresh';
-const REFRESH_COOKIE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30d β matches config.jwt.refreshTtl
-
-// Refresh token travels as an httpOnly cookie β never in a JS-readable
-// response field β so a stolen access token (which docforum-web keeps in
-// memory, never localStorage) can't be paired with a stolen refresh token
-// via the same XSS vector.
-//
-// Cookie attributes come from config.cookie (Phase 8, issue #37): genuinely
-// environment-aware β `SameSite=None; Secure` in production, where
-// docforum-web (GitHub Pages) and this API (Render) are different sites, and
-// `SameSite=Lax`/insecure for local dev over plain http. See
-// docs/adr/0003.
-function setRefreshCookie(res: Response, token: string) {
- res.cookie(REFRESH_COOKIE, token, {
- ...config.cookie,
- httpOnly: true,
- maxAge: REFRESH_COOKIE_MAX_AGE_MS,
- });
-}
-
-/** Coarse request context for the session record β what makes "that other
- * device" recognizable in `GET /auth/sessions`. */
-function sessionContext(req: Request): authService.SessionContext {
- return { ip: req.ip, userAgent: req.headers['user-agent'] };
-}
-
-function sessionResponse(res: Response, session: authService.AuthSession) {
- setRefreshCookie(res, session.refreshToken);
- // `csrfToken` is returned in the body, not as a readable cookie β see
- // middleware/csrf.ts for why a cross-site frontend can't use a cookie for
- // this. docforum-web keeps it in memory alongside the access token.
- res.status(200).json({
- user: session.user,
- accessToken: session.accessToken,
- csrfToken: session.csrfToken,
- sessionId: session.sessionId,
- });
-}
-
-export async function signup(req: Request, res: Response) {
- const input = signupSchema.parse(req.body);
- const session = await authService.signup(
- { ...input, locale: resolveLocale(undefined, req.headers['accept-language']) },
- sessionContext(req),
- );
- sessionResponse(res, session);
-}
-
-export async function login(req: Request, res: Response) {
- const input = loginSchema.parse(req.body);
- try {
- // Express's `req.ip` honours the `trust proxy` setting (app.ts) β it is
- // the whole reason the throttle keys on the IP the client believes it
- // is, rather than Render's proxy address (see config.trustProxy).
- const session = await authService.login({ ...input, ip: req.ip }, sessionContext(req));
- sessionResponse(res, session);
- } catch (err) {
- // Phase 8, issue #35: a throttled attempt must tell the client how long
- // to wait, both as a header and in the message (see LoginThrottledError).
- if (err instanceof authService.LoginThrottledError) {
- res.set('Retry-After', String(err.retryAfterSeconds));
- }
- throw err;
- }
-}
-
-/** Admin-only: clear a login lock/throttle bucket for an account or an IP.
- * Writes an audit line to the server log. Not wired into AuditEvent yet
- * (issue #43 covered booking, doctor verification and payments only β see
- * ARCHITECTURE.md Β§6.2.4), so this is the interim record. */
-export async function clearLoginAttempts(req: Request, res: Response) {
- const { email, ip } = clearLoginAttemptsSchema.parse(req.body);
- const cleared = await authService.clearLoginAttempts({ email, ip });
- // eslint-disable-next-line no-console
- console.info(`[auth] admin ${req.user?.id ?? 'unknown'} cleared ${cleared} login-throttle bucket(s) (email=${email ?? '-'} ip=${ip ?? '-'})`);
- res.status(200).json({ cleared });
-}
-
-/**
- * Admin invite redemption (Phase 8, issue #39). Lives in the auth module
- * because it ends in a session, but the invite itself is the users module's
- * business β see `authService.signupWithInvite`, which calls that module's
- * service rather than touching its tables.
- */
-export async function signupWithInvite(req: Request, res: Response) {
- const input = signupWithInviteSchema.parse(req.body);
- const session = await authService.signupWithInvite(input, sessionContext(req));
- sessionResponse(res, session);
-}
-
-export async function refresh(req: Request, res: Response) {
- const token = refreshCookieValue(req);
- if (!token) throw new HttpError(401, 'No refresh token provided.');
-
- const session = await authService.refresh(token, readCsrfHeader(req));
- sessionResponse(res, session);
-}
-
-// Phase 8, issue #34.
-export async function verifyEmail(req: Request, res: Response) {
- const { token } = verifyEmailSchema.parse(req.body);
- const result = await authService.verifyEmail(token);
- res.status(200).json({ emailVerifiedAt: result.emailVerifiedAt });
-}
-
-/** Authenticated on purpose: the resend target is the caller's own
- * account, so this endpoint can't be used to enumerate registered
- * addresses, and being logged in is exactly the state a user is in when
- * they need it (login is allowed while unverified). */
-export async function resendVerificationEmail(req: Request, res: Response) {
- if (!req.user) throw new HttpError(401, 'Authentication required.');
- const result = await authService.resendVerificationEmail(req.user.id);
- res.status(200).json(result);
-}
-
-/**
- * Clears the refresh cookie.
- *
- * Logout is cookie-authenticated too, so it needs the same CSRF treatment β
- * but there's no session token to bind a CSRF token to here (the request
- * carries no access token), so the enforced layers are the Origin allowlist
- * (`requireAllowedOrigin` on the route) plus the requirement that the
- * `X-CSRF-Token` header be present. A cross-site HTML form can set neither,
- * so a forged logout is refused before it clears anything. The worst case if
- * it weren't β a forced sign-out β is why this is the one route where a
- * presence check is enough rather than a value match.
- */
-export function logout(req: Request, res: Response) {
- if (!readCsrfHeader(req)) {
- throw new HttpError(403, 'Missing CSRF token. Send the csrfToken from your login response in the X-CSRF-Token header.');
- }
- res.clearCookie(REFRESH_COOKIE, config.cookie);
- res.status(204).send();
-}
-
-// Both reset endpoints answer identically for known and unknown emails β
-// same status, same body, no branch on the result of the lookup. Rate
-// limiting is the one intentional exception (429), and it fires before any
-// account lookup, so it can't leak existence either.
-export async function requestPasswordReset(req: Request, res: Response) {
- const { email } = passwordResetRequestSchema.parse(req.body);
- await authService.requestPasswordReset({ email, ip: req.ip ?? 'unknown' });
- res.status(202).json({
- message: 'If an account exists for that email, a password reset link has been sent.',
- });
-}
-
-export async function confirmPasswordReset(req: Request, res: Response) {
- const input = passwordResetConfirmSchema.parse(req.body);
- await authService.confirmPasswordReset(input);
- // The reset already killed every refresh token for this user
- // (tokenVersion bump) β clear the caller's cookie too so a browser that
- // still holds the old one isn't left sending a dead token.
- res.clearCookie(REFRESH_COOKIE, config.cookie);
- res.status(200).json({ message: 'Password updated. Please sign in with your new password.' });
-}
-
-// --- Session management (Phase 8 #38) -----------------------------------
-
-function refreshCookieValue(req: Request): string | undefined {
- return (req.cookies as Record | undefined)?.[REFRESH_COOKIE];
-}
-
-function requireUserId(req: Request): string {
- if (!req.user) throw new HttpError(401, 'Authentication required.');
- return req.user.id;
-}
-
-export async function listSessions(req: Request, res: Response) {
- const sessions = await authService.listSessions({
- userId: requireUserId(req),
- currentSessionId: authService.sessionIdFromRefreshToken(refreshCookieValue(req)),
- });
- res.status(200).json(sessions);
-}
-
-/** Revokes one of the caller's own sessions. A session id belonging to
- * someone else is simply not found (404) β scoped update, and no signal
- * about whether that id exists. */
-export async function revokeSession(req: Request, res: Response) {
- const currentSessionId = authService.sessionIdFromRefreshToken(refreshCookieValue(req));
- const result = await authService.revokeSession({
- userId: requireUserId(req),
- sessionId: getParam(req, 'id'),
- currentSessionId,
- });
-
- if (!result.revoked) throw new HttpError(404, 'Session not found.');
- // Revoking the session you're using has to clear the cookie in the same
- // response, or the browser keeps sending a dead refresh token.
- if (result.wasCurrent) res.clearCookie(REFRESH_COOKIE, config.cookie);
- res.status(204).send();
-}
-
-/** "Sign out everywhere else" β keeps the caller's current session. */
-export async function revokeOtherSessions(req: Request, res: Response) {
- const revoked = await authService.revokeOtherSessions({
- userId: requireUserId(req),
- currentSessionId: authService.sessionIdFromRefreshToken(refreshCookieValue(req)),
- });
- res.status(200).json({ revoked });
-}
-
-/** Admin: revoke every session a given user has. Mounted under /admin, not
- * /auth β it acts on someone else's sessions. */
-export async function revokeUserSessions(req: Request, res: Response) {
- const revoked = await authService.revokeAllUserSessions(getParam(req, 'id'));
- res.status(200).json({ revoked });
-}
+import type { Request, Response } from 'express';
+import * as authService from '../services';
+import {
+ clearLoginAttemptsSchema,
+ loginSchema,
+ passwordResetConfirmSchema,
+ passwordResetRequestSchema,
+ signupSchema,
+ signupWithInviteSchema,
+ verifyEmailSchema,
+} from '../validation';
+import { HttpError } from '../../../http-error';
+import { config } from '../../../config';
+import { readCsrfHeader } from '../../../middleware/csrf';
+import { getParam } from '../../../request-params';
+import { resolveLocale } from '../../../i18n';
+
+const REFRESH_COOKIE = 'docforum_refresh';
+const REFRESH_COOKIE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30d β matches config.jwt.refreshTtl
+
+// Refresh token travels as an httpOnly cookie β never in a JS-readable
+// response field β so a stolen access token (which docforum-web keeps in
+// memory, never localStorage) can't be paired with a stolen refresh token
+// via the same XSS vector.
+//
+// Cookie attributes come from config.cookie (Phase 8, issue #37): genuinely
+// environment-aware β `SameSite=None; Secure` in production, where
+// docforum-web (GitHub Pages) and this API (Render) are different sites, and
+// `SameSite=Lax`/insecure for local dev over plain http. See
+// docs/adr/0003.
+function setRefreshCookie(res: Response, token: string) {
+ res.cookie(REFRESH_COOKIE, token, {
+ ...config.cookie,
+ httpOnly: true,
+ maxAge: REFRESH_COOKIE_MAX_AGE_MS,
+ });
+}
+
+/** Coarse request context for the session record β what makes "that other
+ * device" recognizable in `GET /auth/sessions`. */
+function sessionContext(req: Request): authService.SessionContext {
+ return { ip: req.ip, userAgent: req.headers['user-agent'] };
+}
+
+function sessionResponse(res: Response, session: authService.AuthSession) {
+ setRefreshCookie(res, session.refreshToken);
+ // `csrfToken` is returned in the body, not as a readable cookie β see
+ // middleware/csrf.ts for why a cross-site frontend can't use a cookie for
+ // this. docforum-web keeps it in memory alongside the access token.
+ res.status(200).json({
+ user: session.user,
+ accessToken: session.accessToken,
+ csrfToken: session.csrfToken,
+ sessionId: session.sessionId,
+ });
+}
+
+export async function signup(req: Request, res: Response) {
+ const input = signupSchema.parse(req.body);
+ const session = await authService.signup(
+ { ...input, locale: resolveLocale(undefined, req.headers['accept-language']) },
+ sessionContext(req),
+ );
+ sessionResponse(res, session);
+}
+
+export async function login(req: Request, res: Response) {
+ const input = loginSchema.parse(req.body);
+ try {
+ // Express's `req.ip` honours the `trust proxy` setting (app.ts) β it is
+ // the whole reason the throttle keys on the IP the client believes it
+ // is, rather than Render's proxy address (see config.trustProxy).
+ const session = await authService.login({ ...input, ip: req.ip }, sessionContext(req));
+ sessionResponse(res, session);
+ } catch (err) {
+ // Phase 8, issue #35: a throttled attempt must tell the client how long
+ // to wait, both as a header and in the message (see LoginThrottledError).
+ if (err instanceof authService.LoginThrottledError) {
+ res.set('Retry-After', String(err.retryAfterSeconds));
+ }
+ throw err;
+ }
+}
+
+/** Admin-only: clear a login lock/throttle bucket for an account or an IP.
+ * Writes an audit line to the server log. Not wired into AuditEvent yet
+ * (issue #43 covered booking, doctor verification and payments only β see
+ * ARCHITECTURE.md Β§6.2.4), so this is the interim record. */
+export async function clearLoginAttempts(req: Request, res: Response) {
+ const { email, ip } = clearLoginAttemptsSchema.parse(req.body);
+ const cleared = await authService.clearLoginAttempts({ email, ip });
+ // eslint-disable-next-line no-console
+ console.info(`[auth] admin ${req.user?.id ?? 'unknown'} cleared ${cleared} login-throttle bucket(s) (email=${email ?? '-'} ip=${ip ?? '-'})`);
+ res.status(200).json({ cleared });
+}
+
+/**
+ * Admin invite redemption (Phase 8, issue #39). Lives in the auth module
+ * because it ends in a session, but the invite itself is the users module's
+ * business β see `authService.signupWithInvite`, which calls that module's
+ * service rather than touching its tables.
+ */
+export async function signupWithInvite(req: Request, res: Response) {
+ const input = signupWithInviteSchema.parse(req.body);
+ const session = await authService.signupWithInvite(input, sessionContext(req));
+ sessionResponse(res, session);
+}
+
+export async function refresh(req: Request, res: Response) {
+ const token = refreshCookieValue(req);
+ if (!token) throw new HttpError(401, 'No refresh token provided.');
+
+ const session = await authService.refresh(token, readCsrfHeader(req));
+ sessionResponse(res, session);
+}
+
+// Phase 8, issue #34.
+export async function verifyEmail(req: Request, res: Response) {
+ const { token } = verifyEmailSchema.parse(req.body);
+ const result = await authService.verifyEmail(token);
+ res.status(200).json({ emailVerifiedAt: result.emailVerifiedAt });
+}
+
+/** Authenticated on purpose: the resend target is the caller's own
+ * account, so this endpoint can't be used to enumerate registered
+ * addresses, and being logged in is exactly the state a user is in when
+ * they need it (login is allowed while unverified). */
+export async function resendVerificationEmail(req: Request, res: Response) {
+ if (!req.user) throw new HttpError(401, 'Authentication required.');
+ const result = await authService.resendVerificationEmail(req.user.id);
+ res.status(200).json(result);
+}
+
+/**
+ * Clears the refresh cookie.
+ *
+ * Logout is cookie-authenticated too, so it needs the same CSRF treatment β
+ * but there's no session token to bind a CSRF token to here (the request
+ * carries no access token), so the enforced layers are the Origin allowlist
+ * (`requireAllowedOrigin` on the route) plus the requirement that the
+ * `X-CSRF-Token` header be present. A cross-site HTML form can set neither,
+ * so a forged logout is refused before it clears anything. The worst case if
+ * it weren't β a forced sign-out β is why this is the one route where a
+ * presence check is enough rather than a value match.
+ */
+export function logout(req: Request, res: Response) {
+ if (!readCsrfHeader(req)) {
+ throw new HttpError(403, 'Missing CSRF token. Send the csrfToken from your login response in the X-CSRF-Token header.');
+ }
+ res.clearCookie(REFRESH_COOKIE, config.cookie);
+ res.status(204).send();
+}
+
+// Both reset endpoints answer identically for known and unknown emails β
+// same status, same body, no branch on the result of the lookup. Rate
+// limiting is the one intentional exception (429), and it fires before any
+// account lookup, so it can't leak existence either.
+export async function requestPasswordReset(req: Request, res: Response) {
+ const { email } = passwordResetRequestSchema.parse(req.body);
+ await authService.requestPasswordReset({ email, ip: req.ip ?? 'unknown' });
+ res.status(202).json({
+ message: 'If an account exists for that email, a password reset link has been sent.',
+ });
+}
+
+export async function confirmPasswordReset(req: Request, res: Response) {
+ const input = passwordResetConfirmSchema.parse(req.body);
+ await authService.confirmPasswordReset(input);
+ // The reset already killed every refresh token for this user
+ // (tokenVersion bump) β clear the caller's cookie too so a browser that
+ // still holds the old one isn't left sending a dead token.
+ res.clearCookie(REFRESH_COOKIE, config.cookie);
+ res.status(200).json({ message: 'Password updated. Please sign in with your new password.' });
+}
+
+// --- Session management (Phase 8 #38) -----------------------------------
+
+function refreshCookieValue(req: Request): string | undefined {
+ return (req.cookies as Record | undefined)?.[REFRESH_COOKIE];
+}
+
+function requireUserId(req: Request): string {
+ if (!req.user) throw new HttpError(401, 'Authentication required.');
+ return req.user.id;
+}
+
+export async function listSessions(req: Request, res: Response) {
+ const sessions = await authService.listSessions({
+ userId: requireUserId(req),
+ currentSessionId: authService.sessionIdFromRefreshToken(refreshCookieValue(req)),
+ });
+ res.status(200).json(sessions);
+}
+
+/** Revokes one of the caller's own sessions. A session id belonging to
+ * someone else is simply not found (404) β scoped update, and no signal
+ * about whether that id exists. */
+export async function revokeSession(req: Request, res: Response) {
+ const currentSessionId = authService.sessionIdFromRefreshToken(refreshCookieValue(req));
+ const result = await authService.revokeSession({
+ userId: requireUserId(req),
+ sessionId: getParam(req, 'id'),
+ currentSessionId,
+ });
+
+ if (!result.revoked) throw new HttpError(404, 'Session not found.');
+ // Revoking the session you're using has to clear the cookie in the same
+ // response, or the browser keeps sending a dead refresh token.
+ if (result.wasCurrent) res.clearCookie(REFRESH_COOKIE, config.cookie);
+ res.status(204).send();
+}
+
+/** "Sign out everywhere else" β keeps the caller's current session. */
+export async function revokeOtherSessions(req: Request, res: Response) {
+ const revoked = await authService.revokeOtherSessions({
+ userId: requireUserId(req),
+ currentSessionId: authService.sessionIdFromRefreshToken(refreshCookieValue(req)),
+ });
+ res.status(200).json({ revoked });
+}
+
+/** Admin: revoke every session a given user has. Mounted under /admin, not
+ * /auth β it acts on someone else's sessions. */
+export async function revokeUserSessions(req: Request, res: Response) {
+ const revoked = await authService.revokeAllUserSessions(getParam(req, 'id'));
+ res.status(200).json({ revoked });
+}
diff --git a/backend/src/modules/auth/repositories/index.ts b/backend/src/modules/auth/repositories/index.ts
index d9770280..b5b51a93 100644
--- a/backend/src/modules/auth/repositories/index.ts
+++ b/backend/src/modules/auth/repositories/index.ts
@@ -1,331 +1,331 @@
-// Auth module's repository β the only place that touches the `User` /
-// `PatientProfile` / `DoctorProfile` / `PasswordResetToken` tables for auth
-// purposes. Other modules must go through this module's services, never
-// import this file directly (AGENTS.md hard rule 4).
-import { prisma } from '../../../db/prisma-client';
-import type { Prisma, Role } from '@prisma/client';
-
-export function findUserByEmail(email: string) {
- return prisma.user.findUnique({ where: { email } });
-}
-
-export function findUserById(id: string, db: typeof prisma | Prisma.TransactionClient = prisma) {
- return db.user.findUnique({ where: { id } });
-}
-
-// --- Refresh sessions (Phase 8 #38) -------------------------------------
-
-export function createRefreshSession(input: { userId: string; userAgent?: string; ip?: string }) {
- return prisma.refreshSession.create({
- data: { userId: input.userId, userAgent: input.userAgent, ip: input.ip },
- });
-}
-
-export function findRefreshSessionById(id: string) {
- return prisma.refreshSession.findUnique({ where: { id } });
-}
-
-/** The only write a session takes outside of creation/revocation β called
- * from refresh, not from every authenticated request. */
-export function touchRefreshSession(id: string) {
- return prisma.refreshSession.update({ where: { id }, data: { lastUsedAt: new Date() } });
-}
-
-export function listRefreshSessions(userId: string) {
- return prisma.refreshSession.findMany({
- where: { userId, revokedAt: null },
- orderBy: { lastUsedAt: 'desc' },
- });
-}
-
-/** Scoped by `userId` in the WHERE clause rather than checked afterwards β
- * revoking someone else's session is a 0-row update, not a logic branch
- * that could be got wrong. */
-export function revokeRefreshSession(id: string, userId: string) {
- return prisma.refreshSession.updateMany({
- where: { id, userId, revokedAt: null },
- data: { revokedAt: new Date() },
- });
-}
-
-export function revokeRefreshSessionsForUser(input: { userId: string; exceptSessionId?: string }) {
- return prisma.refreshSession.updateMany({
- where: {
- userId: input.userId,
- revokedAt: null,
- ...(input.exceptSessionId ? { id: { not: input.exceptSessionId } } : {}),
- },
- data: { revokedAt: new Date() },
- });
-}
-
-export interface CreateSelfServeUserInput {
- email: string;
- phone?: string;
- passwordHash: string;
- role: Extract;
- fullName: string;
- locale?: string;
-}
-
-/** Creates the User row and its matching profile row in one transaction β
- * a User without a profile (or vice versa) should never be observable. */
-export function createSelfServeUser(input: CreateSelfServeUserInput) {
- return prisma.$transaction(async (tx) => {
- const user = await tx.user.create({
- data: {
- email: input.email,
- phone: input.phone,
- passwordHash: input.passwordHash,
- role: input.role,
- locale: input.locale ?? 'en',
- },
- });
-
- if (input.role === 'patient') {
- await tx.patientProfile.create({
- data: { userId: user.id, fullName: input.fullName },
- });
- } else {
- await tx.doctorProfile.create({
- data: { userId: user.id, fullName: input.fullName },
- });
- }
-
- return user;
- });
-}
-
-// --- Email verification (Phase 8, issue #34) -----------------------------
-// Only hashes are stored; see services/verification-tokens.ts.
-
-export function findVerificationTokenByHash(tokenHash: string) {
- return prisma.emailVerificationToken.findUnique({ where: { tokenHash } });
-}
-
-export function findLatestVerificationToken(userId: string) {
- return prisma.emailVerificationToken.findFirst({
- where: { userId },
- orderBy: { createdAt: 'desc' },
- });
-}
-
-/** Issues the newest token and retires any still-outstanding one, in one
- * transaction β "latest link wins", so a resend doesn't leave two live
- * links in two inboxes. Retiring is a `consumedAt` write, never a delete:
- * this table is an append-only-ish audit of what was issued. */
-export function replaceVerificationToken(input: {
- userId: string;
- tokenHash: string;
- expiresAt: Date;
-}) {
- return prisma.$transaction(async (tx) => {
- await tx.emailVerificationToken.updateMany({
- where: { userId: input.userId, consumedAt: null },
- data: { consumedAt: new Date() },
- });
- return tx.emailVerificationToken.create({
- data: { userId: input.userId, tokenHash: input.tokenHash, expiresAt: input.expiresAt },
- });
- });
-}
-
-/** Single-use enforcement, the same shape as the booking guarantee: a
- * conditional UPDATE that only matches while the row is still unused, so
- * two concurrent redemptions of one token can't both win. Returns whether
- * *this* caller consumed it. */
-export async function consumeVerificationToken(tokenId: string, consumedAt: Date): Promise {
- const result = await prisma.emailVerificationToken.updateMany({
- where: { id: tokenId, consumedAt: null },
- data: { consumedAt },
- });
- return result.count === 1;
-}
-
-export function markUserEmailVerified(userId: string, verifiedAt: Date) {
- return prisma.user.update({ where: { id: userId }, data: { emailVerifiedAt: verifiedAt } });
-}
-
-// --- Login throttling (Phase 8, issue #35) -------------------------------
-
-export interface ThrottleRow {
- scope: string;
- key: string;
- failedCount: number;
- lastFailedAt: Date;
- lockUntil: Date | null;
-}
-
-/**
- * Records one failed attempt for a bucket, atomically.
- *
- * This is a single INSERT ... ON CONFLICT DO UPDATE, not a read followed by
- * a write: concurrent failed logins for the same account must all be
- * counted, and a read-then-write would silently drop all but the last one.
- * The three CASE expressions all read the *pre-update* row, which is what
- * makes the window reset, the increment and the lock decision consistent
- * with each other inside one statement.
- *
- * `gen_random_uuid()` is built in from Postgres 13 (no extension needed);
- * Prisma normally generates the uuid client-side, which raw SQL can't.
- *
- * The lock is set once per window (`lockUntil IS NULL` guard): a client that
- * keeps hammering a locked account grows the counter but does *not* push the
- * lock's deadline out, so the lock is a bounded wait rather than a way to
- * keep a legitimate user locked out indefinitely. The next window (which
- * clears both the count and the lock) can lock again.
- */
-export async function recordFailedLoginAttempt(input: {
- scope: 'email' | 'ip';
- key: string;
- policy: { windowSeconds: number; lockAfterFailures: number; lockSeconds: number };
-}): Promise {
- const { windowSeconds, lockAfterFailures, lockSeconds } = input.policy;
- const expiredWindow = prisma.$queryRaw`
- INSERT INTO "LoginAttempt" ("id", "scope", "key", "failedCount", "windowStart", "lastFailedAt", "lockUntil", "createdAt", "updatedAt")
- VALUES (gen_random_uuid()::text, ${input.scope}, ${input.key}, 1, NOW(), NOW(), NULL, NOW(), NOW())
- ON CONFLICT ("scope", "key") DO UPDATE SET
- "failedCount" = CASE
- WHEN "LoginAttempt"."windowStart" < NOW() - make_interval(secs => ${windowSeconds}::int) THEN 1
- ELSE "LoginAttempt"."failedCount" + 1
- END,
- "windowStart" = CASE
- WHEN "LoginAttempt"."windowStart" < NOW() - make_interval(secs => ${windowSeconds}::int) THEN NOW()
- ELSE "LoginAttempt"."windowStart"
- END,
- "lockUntil" = CASE
- WHEN "LoginAttempt"."windowStart" < NOW() - make_interval(secs => ${windowSeconds}::int) THEN NULL
- WHEN (CASE
- WHEN "LoginAttempt"."windowStart" < NOW() - make_interval(secs => ${windowSeconds}::int) THEN 1
- ELSE "LoginAttempt"."failedCount" + 1
- END) >= ${lockAfterFailures}::int
- AND "LoginAttempt"."lockUntil" IS NULL
- THEN NOW() + make_interval(secs => ${lockSeconds}::int)
- ELSE "LoginAttempt"."lockUntil"
- END,
- "lastFailedAt" = NOW(),
- "updatedAt" = NOW()
- RETURNING "scope", "key", "failedCount", "lastFailedAt", "lockUntil"
- `;
- const rows = await expiredWindow;
- if (!rows[0]) throw new Error('LoginAttempt upsert returned no row.');
- return rows[0];
-}
-
-export function findThrottleBuckets(emailKey: string, ipKey: string | null): Promise {
- const clauses: { scope: string; key: string }[] = [{ scope: 'email', key: emailKey }];
- if (ipKey) clauses.push({ scope: 'ip', key: ipKey });
- return prisma.loginAttempt.findMany({
- where: { OR: clauses },
- select: { scope: true, key: true, failedCount: true, lastFailedAt: true, lockUntil: true },
- });
-}
-
-export function clearLoginAttempts(buckets: { scope: string; key: string }[]) {
- if (buckets.length === 0) return Promise.resolve(0);
- return prisma.loginAttempt
- .deleteMany({ where: { OR: buckets.map((bucket) => ({ scope: bucket.scope, key: bucket.key })) } })
- .then((result) => result.count);
-}
-
-/** Cleanup: drop buckets whose last failure is older than `before`. */
-export function pruneStaleLoginAttempts(before: Date) {
- return prisma.loginAttempt.deleteMany({ where: { lastFailedAt: { lt: before } } }).then((result) => result.count);
-}
-
-/**
- * Stores a new single-use reset token for a user, superseding any of that
- * user's outstanding ones in the same transaction β "requesting a second
- * reset invalidates the first token" is a DB-level guarantee here, not an
- * application-level hope.
- */
-export function createPasswordResetToken(input: {
- userId: string;
- tokenHash: string;
- expiresAt: Date;
-}) {
- return prisma.$transaction(async (tx) => {
- await tx.passwordResetToken.updateMany({
- where: { userId: input.userId, usedAt: null },
- data: { usedAt: new Date() },
- });
- return tx.passwordResetToken.create({
- data: { userId: input.userId, tokenHash: input.tokenHash, expiresAt: input.expiresAt },
- });
- });
-}
-
-/** Lookup by token hash (unique index) β never by the raw token, which is
- * never stored. */
-export function findPasswordResetTokenByHash(tokenHash: string) {
- return prisma.passwordResetToken.findUnique({ where: { tokenHash } });
-}
-
-/**
- * Consumes a reset token and applies the new password in one transaction:
- * marks the token used, sets the new password hash, and increments
- * `User.tokenVersion` so every previously issued refresh token for this
- * user stops working (issue #33; the full revocation list is #18).
- */
-export function consumePasswordResetToken(input: {
- tokenId: string;
- userId: string;
- passwordHash: string;
-}) {
- return prisma.$transaction(async (tx) => {
- const claimed = await tx.passwordResetToken.updateMany({
- where: { id: input.tokenId, usedAt: null },
- data: { usedAt: new Date() },
- });
- // Two concurrent confirms of the same token: only one claims the row.
- if (claimed.count !== 1) return false;
-
- await tx.user.update({
- where: { id: input.userId },
- data: { passwordHash: input.passwordHash, tokenVersion: { increment: 1 } },
- });
- return true;
- });
-}
-
-// --- Account deletion (Phase 8, issue #41, docs/adr/0010) ----------------
-// Called inside the patients module's deletion transaction, so every write
-// here takes the caller's transaction client.
-
-type DbClient = typeof prisma | Prisma.TransactionClient;
-
-/** Anonymises the User row in place. Conditional on `deletedAt IS NULL`, so
- * of two concurrent deletions exactly one does the work; returns whether
- * this call did. */
-export async function anonymizeUser(
- db: DbClient,
- userId: string,
- data: { tombstoneEmail: string; unusablePasswordHash: string; deletedAt: Date },
-): Promise {
- const result = await db.user.updateMany({
- where: { id: userId, deletedAt: null },
- data: {
- email: data.tombstoneEmail,
- phone: null,
- passwordHash: data.unusablePasswordHash,
- emailVerifiedAt: null,
- deletedAt: data.deletedAt,
- },
- });
- return result.count === 1;
-}
-
-export function deleteVerificationTokensForUser(db: DbClient, userId: string) {
- return db.emailVerificationToken.deleteMany({ where: { userId } }).then((result) => result.count);
-}
-
-export function deleteLoginAttemptBucket(db: DbClient, scope: 'email' | 'ip', key: string) {
- return db.loginAttempt.deleteMany({ where: { scope, key } }).then((result) => result.count);
-}
-
-/** Hard-deletes an already-anonymised User (tombstone purge). Conditional
- * on `deletedAt` being set, so a live account can never be removed here. */
-export async function deleteAnonymizedUser(db: DbClient, userId: string): Promise {
- const result = await db.user.deleteMany({ where: { id: userId, deletedAt: { not: null } } });
- return result.count === 1;
-}
+// Auth module's repository β the only place that touches the `User` /
+// `PatientProfile` / `DoctorProfile` / `PasswordResetToken` tables for auth
+// purposes. Other modules must go through this module's services, never
+// import this file directly (AGENTS.md hard rule 4).
+import { prisma } from '../../../db/prisma-client';
+import type { Prisma, Role } from '@prisma/client';
+
+export function findUserByEmail(email: string) {
+ return prisma.user.findUnique({ where: { email } });
+}
+
+export function findUserById(id: string, db: typeof prisma | Prisma.TransactionClient = prisma) {
+ return db.user.findUnique({ where: { id } });
+}
+
+// --- Refresh sessions (Phase 8 #38) -------------------------------------
+
+export function createRefreshSession(input: { userId: string; userAgent?: string; ip?: string }) {
+ return prisma.refreshSession.create({
+ data: { userId: input.userId, userAgent: input.userAgent, ip: input.ip },
+ });
+}
+
+export function findRefreshSessionById(id: string) {
+ return prisma.refreshSession.findUnique({ where: { id } });
+}
+
+/** The only write a session takes outside of creation/revocation β called
+ * from refresh, not from every authenticated request. */
+export function touchRefreshSession(id: string) {
+ return prisma.refreshSession.update({ where: { id }, data: { lastUsedAt: new Date() } });
+}
+
+export function listRefreshSessions(userId: string) {
+ return prisma.refreshSession.findMany({
+ where: { userId, revokedAt: null },
+ orderBy: { lastUsedAt: 'desc' },
+ });
+}
+
+/** Scoped by `userId` in the WHERE clause rather than checked afterwards β
+ * revoking someone else's session is a 0-row update, not a logic branch
+ * that could be got wrong. */
+export function revokeRefreshSession(id: string, userId: string) {
+ return prisma.refreshSession.updateMany({
+ where: { id, userId, revokedAt: null },
+ data: { revokedAt: new Date() },
+ });
+}
+
+export function revokeRefreshSessionsForUser(input: { userId: string; exceptSessionId?: string }) {
+ return prisma.refreshSession.updateMany({
+ where: {
+ userId: input.userId,
+ revokedAt: null,
+ ...(input.exceptSessionId ? { id: { not: input.exceptSessionId } } : {}),
+ },
+ data: { revokedAt: new Date() },
+ });
+}
+
+export interface CreateSelfServeUserInput {
+ email: string;
+ phone?: string;
+ passwordHash: string;
+ role: Extract;
+ fullName: string;
+ locale?: string;
+}
+
+/** Creates the User row and its matching profile row in one transaction β
+ * a User without a profile (or vice versa) should never be observable. */
+export function createSelfServeUser(input: CreateSelfServeUserInput) {
+ return prisma.$transaction(async (tx) => {
+ const user = await tx.user.create({
+ data: {
+ email: input.email,
+ phone: input.phone,
+ passwordHash: input.passwordHash,
+ role: input.role,
+ locale: input.locale ?? 'en',
+ },
+ });
+
+ if (input.role === 'patient') {
+ await tx.patientProfile.create({
+ data: { userId: user.id, fullName: input.fullName },
+ });
+ } else {
+ await tx.doctorProfile.create({
+ data: { userId: user.id, fullName: input.fullName },
+ });
+ }
+
+ return user;
+ });
+}
+
+// --- Email verification (Phase 8, issue #34) -----------------------------
+// Only hashes are stored; see services/verification-tokens.ts.
+
+export function findVerificationTokenByHash(tokenHash: string) {
+ return prisma.emailVerificationToken.findUnique({ where: { tokenHash } });
+}
+
+export function findLatestVerificationToken(userId: string) {
+ return prisma.emailVerificationToken.findFirst({
+ where: { userId },
+ orderBy: { createdAt: 'desc' },
+ });
+}
+
+/** Issues the newest token and retires any still-outstanding one, in one
+ * transaction β "latest link wins", so a resend doesn't leave two live
+ * links in two inboxes. Retiring is a `consumedAt` write, never a delete:
+ * this table is an append-only-ish audit of what was issued. */
+export function replaceVerificationToken(input: {
+ userId: string;
+ tokenHash: string;
+ expiresAt: Date;
+}) {
+ return prisma.$transaction(async (tx) => {
+ await tx.emailVerificationToken.updateMany({
+ where: { userId: input.userId, consumedAt: null },
+ data: { consumedAt: new Date() },
+ });
+ return tx.emailVerificationToken.create({
+ data: { userId: input.userId, tokenHash: input.tokenHash, expiresAt: input.expiresAt },
+ });
+ });
+}
+
+/** Single-use enforcement, the same shape as the booking guarantee: a
+ * conditional UPDATE that only matches while the row is still unused, so
+ * two concurrent redemptions of one token can't both win. Returns whether
+ * *this* caller consumed it. */
+export async function consumeVerificationToken(tokenId: string, consumedAt: Date): Promise {
+ const result = await prisma.emailVerificationToken.updateMany({
+ where: { id: tokenId, consumedAt: null },
+ data: { consumedAt },
+ });
+ return result.count === 1;
+}
+
+export function markUserEmailVerified(userId: string, verifiedAt: Date) {
+ return prisma.user.update({ where: { id: userId }, data: { emailVerifiedAt: verifiedAt } });
+}
+
+// --- Login throttling (Phase 8, issue #35) -------------------------------
+
+export interface ThrottleRow {
+ scope: string;
+ key: string;
+ failedCount: number;
+ lastFailedAt: Date;
+ lockUntil: Date | null;
+}
+
+/**
+ * Records one failed attempt for a bucket, atomically.
+ *
+ * This is a single INSERT ... ON CONFLICT DO UPDATE, not a read followed by
+ * a write: concurrent failed logins for the same account must all be
+ * counted, and a read-then-write would silently drop all but the last one.
+ * The three CASE expressions all read the *pre-update* row, which is what
+ * makes the window reset, the increment and the lock decision consistent
+ * with each other inside one statement.
+ *
+ * `gen_random_uuid()` is built in from Postgres 13 (no extension needed);
+ * Prisma normally generates the uuid client-side, which raw SQL can't.
+ *
+ * The lock is set once per window (`lockUntil IS NULL` guard): a client that
+ * keeps hammering a locked account grows the counter but does *not* push the
+ * lock's deadline out, so the lock is a bounded wait rather than a way to
+ * keep a legitimate user locked out indefinitely. The next window (which
+ * clears both the count and the lock) can lock again.
+ */
+export async function recordFailedLoginAttempt(input: {
+ scope: 'email' | 'ip';
+ key: string;
+ policy: { windowSeconds: number; lockAfterFailures: number; lockSeconds: number };
+}): Promise {
+ const { windowSeconds, lockAfterFailures, lockSeconds } = input.policy;
+ const expiredWindow = prisma.$queryRaw`
+ INSERT INTO "LoginAttempt" ("id", "scope", "key", "failedCount", "windowStart", "lastFailedAt", "lockUntil", "createdAt", "updatedAt")
+ VALUES (gen_random_uuid()::text, ${input.scope}, ${input.key}, 1, NOW(), NOW(), NULL, NOW(), NOW())
+ ON CONFLICT ("scope", "key") DO UPDATE SET
+ "failedCount" = CASE
+ WHEN "LoginAttempt"."windowStart" < NOW() - make_interval(secs => ${windowSeconds}::int) THEN 1
+ ELSE "LoginAttempt"."failedCount" + 1
+ END,
+ "windowStart" = CASE
+ WHEN "LoginAttempt"."windowStart" < NOW() - make_interval(secs => ${windowSeconds}::int) THEN NOW()
+ ELSE "LoginAttempt"."windowStart"
+ END,
+ "lockUntil" = CASE
+ WHEN "LoginAttempt"."windowStart" < NOW() - make_interval(secs => ${windowSeconds}::int) THEN NULL
+ WHEN (CASE
+ WHEN "LoginAttempt"."windowStart" < NOW() - make_interval(secs => ${windowSeconds}::int) THEN 1
+ ELSE "LoginAttempt"."failedCount" + 1
+ END) >= ${lockAfterFailures}::int
+ AND "LoginAttempt"."lockUntil" IS NULL
+ THEN NOW() + make_interval(secs => ${lockSeconds}::int)
+ ELSE "LoginAttempt"."lockUntil"
+ END,
+ "lastFailedAt" = NOW(),
+ "updatedAt" = NOW()
+ RETURNING "scope", "key", "failedCount", "lastFailedAt", "lockUntil"
+ `;
+ const rows = await expiredWindow;
+ if (!rows[0]) throw new Error('LoginAttempt upsert returned no row.');
+ return rows[0];
+}
+
+export function findThrottleBuckets(emailKey: string, ipKey: string | null): Promise {
+ const clauses: { scope: string; key: string }[] = [{ scope: 'email', key: emailKey }];
+ if (ipKey) clauses.push({ scope: 'ip', key: ipKey });
+ return prisma.loginAttempt.findMany({
+ where: { OR: clauses },
+ select: { scope: true, key: true, failedCount: true, lastFailedAt: true, lockUntil: true },
+ });
+}
+
+export function clearLoginAttempts(buckets: { scope: string; key: string }[]) {
+ if (buckets.length === 0) return Promise.resolve(0);
+ return prisma.loginAttempt
+ .deleteMany({ where: { OR: buckets.map((bucket) => ({ scope: bucket.scope, key: bucket.key })) } })
+ .then((result) => result.count);
+}
+
+/** Cleanup: drop buckets whose last failure is older than `before`. */
+export function pruneStaleLoginAttempts(before: Date) {
+ return prisma.loginAttempt.deleteMany({ where: { lastFailedAt: { lt: before } } }).then((result) => result.count);
+}
+
+/**
+ * Stores a new single-use reset token for a user, superseding any of that
+ * user's outstanding ones in the same transaction β "requesting a second
+ * reset invalidates the first token" is a DB-level guarantee here, not an
+ * application-level hope.
+ */
+export function createPasswordResetToken(input: {
+ userId: string;
+ tokenHash: string;
+ expiresAt: Date;
+}) {
+ return prisma.$transaction(async (tx) => {
+ await tx.passwordResetToken.updateMany({
+ where: { userId: input.userId, usedAt: null },
+ data: { usedAt: new Date() },
+ });
+ return tx.passwordResetToken.create({
+ data: { userId: input.userId, tokenHash: input.tokenHash, expiresAt: input.expiresAt },
+ });
+ });
+}
+
+/** Lookup by token hash (unique index) β never by the raw token, which is
+ * never stored. */
+export function findPasswordResetTokenByHash(tokenHash: string) {
+ return prisma.passwordResetToken.findUnique({ where: { tokenHash } });
+}
+
+/**
+ * Consumes a reset token and applies the new password in one transaction:
+ * marks the token used, sets the new password hash, and increments
+ * `User.tokenVersion` so every previously issued refresh token for this
+ * user stops working (issue #33; the full revocation list is #18).
+ */
+export function consumePasswordResetToken(input: {
+ tokenId: string;
+ userId: string;
+ passwordHash: string;
+}) {
+ return prisma.$transaction(async (tx) => {
+ const claimed = await tx.passwordResetToken.updateMany({
+ where: { id: input.tokenId, usedAt: null },
+ data: { usedAt: new Date() },
+ });
+ // Two concurrent confirms of the same token: only one claims the row.
+ if (claimed.count !== 1) return false;
+
+ await tx.user.update({
+ where: { id: input.userId },
+ data: { passwordHash: input.passwordHash, tokenVersion: { increment: 1 } },
+ });
+ return true;
+ });
+}
+
+// --- Account deletion (Phase 8, issue #41, docs/adr/0010) ----------------
+// Called inside the patients module's deletion transaction, so every write
+// here takes the caller's transaction client.
+
+type DbClient = typeof prisma | Prisma.TransactionClient;
+
+/** Anonymises the User row in place. Conditional on `deletedAt IS NULL`, so
+ * of two concurrent deletions exactly one does the work; returns whether
+ * this call did. */
+export async function anonymizeUser(
+ db: DbClient,
+ userId: string,
+ data: { tombstoneEmail: string; unusablePasswordHash: string; deletedAt: Date },
+): Promise {
+ const result = await db.user.updateMany({
+ where: { id: userId, deletedAt: null },
+ data: {
+ email: data.tombstoneEmail,
+ phone: null,
+ passwordHash: data.unusablePasswordHash,
+ emailVerifiedAt: null,
+ deletedAt: data.deletedAt,
+ },
+ });
+ return result.count === 1;
+}
+
+export function deleteVerificationTokensForUser(db: DbClient, userId: string) {
+ return db.emailVerificationToken.deleteMany({ where: { userId } }).then((result) => result.count);
+}
+
+export function deleteLoginAttemptBucket(db: DbClient, scope: 'email' | 'ip', key: string) {
+ return db.loginAttempt.deleteMany({ where: { scope, key } }).then((result) => result.count);
+}
+
+/** Hard-deletes an already-anonymised User (tombstone purge). Conditional
+ * on `deletedAt` being set, so a live account can never be removed here. */
+export async function deleteAnonymizedUser(db: DbClient, userId: string): Promise {
+ const result = await db.user.deleteMany({ where: { id: userId, deletedAt: { not: null } } });
+ return result.count === 1;
+}
diff --git a/backend/src/modules/auth/routes/index.ts b/backend/src/modules/auth/routes/index.ts
index 20ee8ccd..de5c9882 100644
--- a/backend/src/modules/auth/routes/index.ts
+++ b/backend/src/modules/auth/routes/index.ts
@@ -1,48 +1,48 @@
-import { Router } from 'express';
-import { authenticate, requireRole } from '../../../middleware/auth';
-import { requireAllowedOrigin } from '../../../middleware/csrf';
-import * as authController from '../controllers';
-
-export const authRouter = Router();
-
-authRouter.post('/signup', authController.signup);
-// Phase 8, issue #39: admin invite redemption (the admin invite itself is
-// issued by `POST /admin/invites` in the users module).
-authRouter.post('/signup-with-invite', authController.signupWithInvite);
-authRouter.post('/login', authController.login);
-
-// Phase 8, issue #34 β email verification.
-authRouter.post('/verify-email', authController.verifyEmail);
-authRouter.post('/verify-email/resend', authenticate, authController.resendVerificationEmail);
-
-// Phase 8, issue #35 β admin escape hatch for a lock a legitimate user is
-// stuck behind (a documented script would do too, but an endpoint is easier
-// to use from the ops console and easier to test).
-authRouter.post('/login-attempts/clear', authenticate, requireRole('admin'), authController.clearLoginAttempts);
-
-// Cookie-authenticated routes (Phase 8, issue #37): `requireAllowedOrigin`
-// rejects a request whose Origin is present and not on the allowlist, and
-// both handlers additionally require an `X-CSRF-Token` header β matched
-// against the claim inside the refresh token on /refresh, present on
-// /logout. See middleware/csrf.ts for the choice and its justification.
-authRouter.post('/refresh', requireAllowedOrigin, authController.refresh);
-authRouter.post('/logout', requireAllowedOrigin, authController.logout);
-
-// Password reset (Phase 8 #33) β both unauthenticated; possession of the
-// emailed single-use token is the credential for the confirm step.
-authRouter.post('/password-reset/request', authController.requestPasswordReset);
-authRouter.post('/password-reset/confirm', authController.confirmPasswordReset);
-
-// Session management (Phase 8 #38). Note the ordering: `DELETE /sessions`
-// (revoke all others) is registered before `DELETE /sessions/:id` β Express
-// matches in registration order, and `/sessions` must not be read as an id.
-authRouter.get('/sessions', authenticate, authController.listSessions);
-authRouter.delete('/sessions', authenticate, authController.revokeOtherSessions);
-authRouter.delete('/sessions/:id', authenticate, authController.revokeSession);
-
-/** Admin routes for the auth module, mounted at `/admin` in app.ts. Session
- * administration acts on *another* user, so it doesn't belong under
- * `/auth`. */
-export const adminAuthRouter = Router();
-
-adminAuthRouter.delete('/users/:id/sessions', authenticate, requireRole('admin'), authController.revokeUserSessions);
+import { Router } from 'express';
+import { authenticate, requireRole } from '../../../middleware/auth';
+import { requireAllowedOrigin } from '../../../middleware/csrf';
+import * as authController from '../controllers';
+
+export const authRouter = Router();
+
+authRouter.post('/signup', authController.signup);
+// Phase 8, issue #39: admin invite redemption (the admin invite itself is
+// issued by `POST /admin/invites` in the users module).
+authRouter.post('/signup-with-invite', authController.signupWithInvite);
+authRouter.post('/login', authController.login);
+
+// Phase 8, issue #34 β email verification.
+authRouter.post('/verify-email', authController.verifyEmail);
+authRouter.post('/verify-email/resend', authenticate, authController.resendVerificationEmail);
+
+// Phase 8, issue #35 β admin escape hatch for a lock a legitimate user is
+// stuck behind (a documented script would do too, but an endpoint is easier
+// to use from the ops console and easier to test).
+authRouter.post('/login-attempts/clear', authenticate, requireRole('admin'), authController.clearLoginAttempts);
+
+// Cookie-authenticated routes (Phase 8, issue #37): `requireAllowedOrigin`
+// rejects a request whose Origin is present and not on the allowlist, and
+// both handlers additionally require an `X-CSRF-Token` header β matched
+// against the claim inside the refresh token on /refresh, present on
+// /logout. See middleware/csrf.ts for the choice and its justification.
+authRouter.post('/refresh', requireAllowedOrigin, authController.refresh);
+authRouter.post('/logout', requireAllowedOrigin, authController.logout);
+
+// Password reset (Phase 8 #33) β both unauthenticated; possession of the
+// emailed single-use token is the credential for the confirm step.
+authRouter.post('/password-reset/request', authController.requestPasswordReset);
+authRouter.post('/password-reset/confirm', authController.confirmPasswordReset);
+
+// Session management (Phase 8 #38). Note the ordering: `DELETE /sessions`
+// (revoke all others) is registered before `DELETE /sessions/:id` β Express
+// matches in registration order, and `/sessions` must not be read as an id.
+authRouter.get('/sessions', authenticate, authController.listSessions);
+authRouter.delete('/sessions', authenticate, authController.revokeOtherSessions);
+authRouter.delete('/sessions/:id', authenticate, authController.revokeSession);
+
+/** Admin routes for the auth module, mounted at `/admin` in app.ts. Session
+ * administration acts on *another* user, so it doesn't belong under
+ * `/auth`. */
+export const adminAuthRouter = Router();
+
+adminAuthRouter.delete('/users/:id/sessions', authenticate, requireRole('admin'), authController.revokeUserSessions);
diff --git a/backend/src/modules/auth/services/index.ts b/backend/src/modules/auth/services/index.ts
index f9ca0e32..c2baaf10 100644
--- a/backend/src/modules/auth/services/index.ts
+++ b/backend/src/modules/auth/services/index.ts
@@ -1,596 +1,596 @@
-// Auth services β the only exported surface other modules (and this
-// module's own controllers) may call. Never import repositories/* from
-// outside this module.
-import { config } from '../../../config';
-import { HttpError } from '../../../http-error';
-import {
- anonymizeUser,
- consumeVerificationToken,
- createSelfServeUser,
- deleteAnonymizedUser,
- deleteLoginAttemptBucket,
- deleteVerificationTokensForUser,
- findLatestVerificationToken,
- findUserByEmail,
- findUserById,
- findVerificationTokenByHash,
- markUserEmailVerified,
- replaceVerificationToken,
-} from '../repositories';
-import { hashPassword, verifyPassword } from './password';
-import { signAccessToken, signRefreshToken, verifyRefreshToken } from './tokens';
-import { assertSessionUsable, recordSessionUse, startSession, type SessionContext } from './sessions';
-import { generateVerificationToken, hashVerificationToken } from './verification-tokens';
-import { sendEmail } from './email-dispatcher';
-import {
- LoginThrottledError,
- checkLoginThrottle,
- emailThrottleKey,
- normalizeClientIp,
- registerFailedLogin,
- resetLoginThrottle,
-} from './login-throttle';
-import { csrfTokensMatch, generateCsrfToken } from '../../../middleware/csrf';
-import { redeemAdminInvite } from '../../users/services';
-import type { PatientExportSection } from '../../patients/services';
-import type { Locale } from '../../../i18n';
-import { formatMessage, resolveLocale } from '../../../i18n';
-import { randomBytes } from 'node:crypto';
-import type { Prisma, Role } from '@prisma/client';
-
-// Re-exported so middleware/auth.ts (shared infra, not a peer module) can
-// verify tokens through this module's service surface rather than
-// reaching into services/tokens.ts directly.
-export { verifyAccessToken } from './tokens';
-export type { AccessTokenPayload } from './tokens';
-
-// Password reset (Phase 8 #33) lives in its own file; re-exported here so
-// controllers keep calling a single service surface.
-export {
- confirmPasswordReset,
- hashResetToken,
- requestPasswordReset,
- resetPasswordResetRateLimits,
- type ConfirmPasswordResetInput,
- type RequestPasswordResetInput,
-} from './password-reset';
-
-// Hashing is auth's business (issue #14): the admin-only facility-creation
-// path in the facilities module hashes the initial password through this
-// service surface rather than depending on bcrypt directly.
-export { hashPassword, verifyPassword } from './password';
-
-export {
- assertSessionUsable,
- listSessions,
- recordSessionUse,
- revokeAllUserSessions,
- revokeOtherSessions,
- revokeSession,
- startSession,
- type SessionContext,
- type SessionSummary,
-} from './sessions';
-
-// Re-exported for the same reason: tests (and, at Phase 6, the provider
-// integration) replace the outbound email sender through this module's
-// service surface, not by importing the dispatcher file directly.
-export { setEmailSender, resetEmailSender } from './email-dispatcher';
-export type { EmailMessage, EmailSender } from './email-dispatcher';
-// Login throttling (Phase 8, issue #35) β the login controller needs the
-// error type to set a `Retry-After` header, and src/server.ts runs the
-// cleanup job. Both go through this module's service surface.
-export { LoginThrottledError, clearLoginAttempts, pruneLoginAttempts } from './login-throttle';
-export type { ThrottleDecision } from './login-throttle';
-
-export interface AuthenticatedUser {
- id: string;
- email: string;
- role: Role;
- locale: string;
- // Phase 8, issue #34 β exposed so docforum-web can gate booking UI on it
- // without a second round trip. The server-side guard is the real
- // enforcement point (`assertEmailVerified`).
- emailVerifiedAt: Date | null;
- createdAt: Date;
- updatedAt: Date;
-}
-
-export interface AuthSession {
- user: AuthenticatedUser;
- accessToken: string;
- refreshToken: string;
- /** Phase 8, issue #37 β handed to the client in the response body so it can
- * echo it back in the `X-CSRF-Token` header on cookie-authenticated routes.
- * It deliberately is NOT a JS-readable cookie: docforum-web is a different
- * site from this API, so it couldn't read one. */
- csrfToken: string;
- /** The refresh-session id this pair belongs to (Phase 8 #38) β lets the
- * client mark its own entry in `GET /auth/sessions` as "this device". */
- sessionId: string;
-}
-
-function toPublicUser(user: {
- id: string;
- email: string;
- role: Role;
- locale?: string;
- emailVerifiedAt: Date | null;
- createdAt: Date;
- updatedAt: Date;
-}): AuthenticatedUser {
- return {
- id: user.id,
- email: user.email,
- role: user.role,
- locale: user.locale ?? 'en',
- emailVerifiedAt: user.emailVerifiedAt,
- createdAt: user.createdAt,
- updatedAt: user.updatedAt,
- };
-}
-
-function issueTokensForSession(
- user: { id: string; role: Role; tokenVersion?: number; locale?: string },
- sessionId: string,
-): { accessToken: string; refreshToken: string; csrfToken: string } {
- const payload = { sub: user.id, role: user.role, tv: user.tokenVersion ?? 0, locale: user.locale ?? 'en' };
- // A fresh CSRF token per session, and per rotation β bound into the
- // refresh token so verifying it needs no server-side state (issue #37).
- const csrfToken = generateCsrfToken();
- // `sid` rides on the refresh token only: access tokens stay stateless so
- // no protected request needs a session lookup (issue #38's "no write
- // hot-spot" requirement).
- return {
- accessToken: signAccessToken(payload),
- refreshToken: signRefreshToken({ ...payload, csrf: csrfToken, sid: sessionId }),
- csrfToken,
- };
-}
-
-/** Starts a new session (one per login) and issues its first token pair. */
-async function issueNewSession(
- user: { id: string; role: Role; tokenVersion?: number; locale?: string },
- context: SessionContext,
-): Promise<{ accessToken: string; refreshToken: string; csrfToken: string; sessionId: string }> {
- const session = await startSession({ userId: user.id, ...context });
- return { ...issueTokensForSession(user, session.id), sessionId: session.id };
-}
-
-export interface SignupInput {
- role: Extract;
- email: string;
- password: string;
- fullName: string;
- locale?: Locale;
-}
-
-export async function signup(input: SignupInput, context: SessionContext = {}): Promise {
- // Facility accounts are admin-invited only (PRD OQ-2) β enforced here,
- // not just in docforum-web's UI. `Extract` on
- // the type already narrows this at compile time; this is the runtime
- // guard for a request that bypasses the frontend entirely.
- if (input.role !== 'patient' && input.role !== 'doctor') {
- throw new HttpError(400, 'Self-serve signup is only available for patient or doctor accounts.');
- }
-
- const existing = await findUserByEmail(input.email);
- if (existing) {
- throw new HttpError(409, 'An account with this email already exists.');
- }
-
- const passwordHash = await hashPassword(input.password);
- const user = await createSelfServeUser({
- email: input.email,
- passwordHash,
- role: input.role,
- fullName: input.fullName,
- locale: input.locale,
- });
-
- // Phase 8, issue #34: the account is created and usable for browsing
- // immediately, but unverified β mutating routes that depend on a
- // trustworthy contact address are gated by `assertEmailVerified` until
- // the link is redeemed.
- await issueEmailVerification(user);
-
- const tokens = await issueNewSession(user, context);
- return { user: toPublicUser(user), ...tokens };
-}
-
-/**
- * Redeems an admin invite and signs the new admin in (Phase 8, issue #39).
- *
- * The invite itself belongs to the users module β this module only hashes
- * the password and issues the session, calling that module's service rather
- * than its repository (AGENTS.md hard rule 4).
- */
-export interface SignupWithInviteInput {
- token: string;
- email: string;
- password: string;
-}
-
-export async function signupWithInvite(input: SignupWithInviteInput, context: SessionContext = {}): Promise {
- const passwordHash = await hashPassword(input.password);
- const user = await redeemAdminInvite({ token: input.token, email: input.email, passwordHash });
-
- const tokens = await issueNewSession(user, context);
- return {
- user: {
- id: user.id,
- email: user.email,
- role: user.role,
- locale: user.locale,
- // No verification email is sent on the invite path β the invite itself
- // is the admin-vetted proof of address, so this stays null rather than
- // claiming a verification that never happened.
- emailVerifiedAt: null,
- createdAt: user.createdAt,
- updatedAt: user.updatedAt,
- },
- ...tokens,
- };
-}
-
-export interface LoginInput {
- email: string;
- password: string;
- /** Client IP as Express resolved it (see TRUST_PROXY in config). Optional
- * so non-HTTP callers/tests can omit it β the IP bucket is then simply
- * not tracked for that attempt. */
- ip?: string | null;
-}
-
-/** A real bcrypt hash (of a random string nobody knows) that a login for a
- * non-existent address is compared against. Without it, an unknown email
- * returns in ~0ms while a known one spends ~300ms in bcrypt β a timing
- * side channel that answers "does this account exist?" even though the
- * response body is identical. */
-const ABSENT_ACCOUNT_HASH = '$2b$12$z8J5gwFm9TiwC.YpeD6yguMkgakFPA8egT1KEKYiaeVkmb3CHRGXu';
-
-export async function login(input: LoginInput, context: SessionContext = {}): Promise {
- const throttleKeys = {
- emailKey: emailThrottleKey(input.email),
- ipKey: normalizeClientIp(input.ip),
- };
-
- // Before the password is even looked at: a locked or backing-off bucket is
- // refused identically for a real and a non-existent address (issue #35).
- const gate = await checkLoginThrottle(throttleKeys);
- if (gate.blocked) {
- // Still counted: backoff starts below the lock threshold, so if gated
- // attempts weren't recorded the lock could never be reached. These are
- // failed attempts either way (no credential was accepted).
- const afterFailure = await registerFailedLogin(throttleKeys);
- throw new LoginThrottledError(Math.max(gate.retryAfterSeconds, afterFailure.retryAfterSeconds));
- }
-
- const user = await findUserByEmail(input.email);
- // Same message whether the email doesn't exist or the password is wrong β
- // and the same *cost*, hence the dummy comparison for an absent account.
- const valid = await verifyPassword(input.password, user?.passwordHash ?? ABSENT_ACCOUNT_HASH);
- // `deletedAt` (issue #41): a deleted account's password is already
- // unusable and its address is a tombstone, so this is defence in depth β
- // and it answers exactly like a wrong password.
- if (!user || !valid || user.deletedAt) {
- const afterFailure = await registerFailedLogin(throttleKeys);
- if (afterFailure.blocked) throw new LoginThrottledError(afterFailure.retryAfterSeconds);
- throw new HttpError(401, 'Invalid email or password.');
- }
-
- await resetLoginThrottle(throttleKeys.emailKey);
-
- // Checked *after* the password: a deactivated account must not be
- // distinguishable from a non-existent one without knowing the password
- // (Phase 8, issue #39).
- if (user.deactivatedAt) {
- throw new HttpError(403, 'This account has been deactivated. Contact an administrator.');
- }
-
- const tokens = await issueNewSession(user, context);
- return { user: toPublicUser(user), ...tokens };
-}
-
-// --- Email verification (Phase 8, issue #34) -----------------------------
-
-/** Mints a fresh token, retires any outstanding one, and hands the link to
- * the email dispatcher. The raw token is never persisted and never logged. */
-async function issueEmailVerification(user: { id: string; email: string; locale?: string }): Promise {
- const token = generateVerificationToken();
- const expiresAt = new Date(Date.now() + config.auth.emailVerification.tokenTtlMinutes * 60_000);
-
- await replaceVerificationToken({ userId: user.id, tokenHash: hashVerificationToken(token), expiresAt });
-
- const link = `${config.auth.emailVerification.landingUrl}?token=${encodeURIComponent(token)}`;
- const locale = resolveLocale(user.locale);
- const minutes = config.auth.emailVerification.tokenTtlMinutes;
- try {
- await sendEmail({
- to: user.email,
- subject: formatMessage(locale, 'notifications.emailVerification.subject'),
- text: formatMessage(locale, 'notifications.emailVerification.body', { minutes, link }),
- });
- } catch (err) {
- // A provider hiccup must not fail signup β the account exists and is
- // usable, and the user can hit resend. Logged without the message body.
- // eslint-disable-next-line no-console
- console.error('[auth] verification email dispatch failed:', err instanceof Error ? err.message : err);
- }
-}
-
-export interface VerifyEmailResult {
- emailVerifiedAt: Date;
-}
-
-export async function verifyEmail(rawToken: string): Promise {
- const record = await findVerificationTokenByHash(hashVerificationToken(rawToken));
- if (!record) {
- throw new HttpError(400, 'Invalid email verification token.', 'INVALID_VERIFICATION_TOKEN');
- }
- if (record.consumedAt) {
- throw new HttpError(
- 400,
- 'This verification link has already been used. Request a new one if your email is still unverified.',
- 'INVALID_VERIFICATION_TOKEN',
- );
- }
- if (record.expiresAt.getTime() <= Date.now()) {
- throw new HttpError(400, 'This verification link has expired. Request a new one.', 'VERIFICATION_TOKEN_EXPIRED');
- }
-
- const now = new Date();
- // Conditional, not read-then-write β two tabs redeeming the same link
- // must not both report success.
- const consumed = await consumeVerificationToken(record.id, now);
- if (!consumed) {
- throw new HttpError(400, 'This verification link has already been used.', 'INVALID_VERIFICATION_TOKEN');
- }
-
- const user = await markUserEmailVerified(record.userId, now);
- return { emailVerifiedAt: user.emailVerifiedAt ?? now };
-}
-
-export interface ResendVerificationResult {
- resendAfterSeconds: number;
-}
-
-/** Rate-limited so an unverified account can't be used to flood an inbox.
- * Account-scoped rather than IP-scoped on purpose, and the endpoint is
- * authenticated, so it can't be used to probe which addresses exist. */
-export async function resendVerificationEmail(userId: string): Promise {
- const user = await findUserById(userId);
- if (!user) throw new HttpError(401, 'Authentication required.');
-
- if (user.emailVerifiedAt) {
- throw new HttpError(409, 'This email address is already verified.', 'EMAIL_ALREADY_VERIFIED');
- }
-
- const intervalSeconds = config.auth.emailVerification.resendIntervalSeconds;
- const latest = await findLatestVerificationToken(userId);
- if (latest) {
- const waitedSeconds = (Date.now() - latest.createdAt.getTime()) / 1000;
- if (waitedSeconds < intervalSeconds) {
- throw new HttpError(
- 429,
- `Please wait ${Math.ceil(intervalSeconds - waitedSeconds)}s before requesting another verification email.`,
- 'EMAIL_VERIFICATION_RATE_LIMITED',
- );
- }
- }
-
- await issueEmailVerification(user);
- return { resendAfterSeconds: intervalSeconds };
-}
-
-/** The enforcement point for "unverified accounts can log in and browse, but
- * not book or pay" (issue #34). Deliberately a DB read rather than a claim
- * baked into the access token: verifying in another tab then takes effect
- * immediately instead of after the token's 15-minute TTL. */
-export async function assertEmailVerified(userId: string): Promise {
- const user = await findUserById(userId);
- if (!user) throw new HttpError(401, 'Authentication required.');
- if (!user.emailVerifiedAt) {
- throw new HttpError(403, 'Verify your email address before continuing.', 'EMAIL_NOT_VERIFIED');
- }
-}
-
-/**
- * Exchanges the refresh cookie for a new session.
- *
- * `csrfToken` is the `X-CSRF-Token` header value (Phase 8, issue #37),
- * checked against the claim inside the presented refresh token. The cookie
- * is verified first so a garbage/missing cookie is still a 401, and a
- * *valid* cookie without a matching CSRF token is a 403.
- */
-export async function refresh(refreshToken: string, csrfToken?: string | null): Promise {
- let payload;
- try {
- payload = verifyRefreshToken(refreshToken);
- } catch {
- throw new HttpError(401, 'Invalid or expired refresh token.');
- }
-
- if (!payload.csrf || !csrfToken || !csrfTokensMatch(csrfToken, payload.csrf)) {
- throw new HttpError(
- 403,
- 'Missing or invalid CSRF token. Send the csrfToken from your login response in the X-CSRF-Token header.',
- );
- }
-
- const user = await findUserById(payload.sub);
- // A deleted account (issue #41) can't refresh its way back in.
- if (!user || user.deletedAt) throw new HttpError(401, 'Invalid or expired refresh token.');
-
- // Deactivated accounts can't refresh out of their deactivation, and a
- // session revoked by deactivation/reactivation can't come back to life
- // through a refresh cookie either (Phase 8, issue #39).
- if (user.deactivatedAt) throw new HttpError(403, 'This account has been deactivated.');
-
- // `tv` check: a password reset (or anything else that bumps
- // `User.tokenVersion`) kills every refresh token issued before it. Tokens
- // predating this field carry no `tv` and read as version 0, which is
- // exactly right β they stay valid only while the user has never had a
- // version bump.
- if ((payload.tv ?? 0) !== user.tokenVersion) {
- throw new HttpError(401, 'Invalid or expired refresh token.');
- }
-
- if (isSessionRevoked(user.sessionsInvalidatedAt, payload.iat)) {
- throw new HttpError(401, 'Your session has been revoked. Please sign in again.');
- }
-
- // Tokens minted before session records existed carry no `sid` and are
- // rejected: the migration logs existing sessions out once, which is the
- // honest trade for actually having a revocation store.
- if (!payload.sid) throw new HttpError(401, 'Invalid or expired refresh token.');
-
- await assertSessionUsable({ sessionId: payload.sid, userId: user.id });
- await recordSessionUse(payload.sid);
-
- // Rotation keeps the same session (same family, same `sid`) β a new
- // session row per refresh would make "list my sessions" useless.
- const tokens = issueTokensForSession(user, payload.sid);
- return { user: toPublicUser(user), ...tokens, sessionId: payload.sid };
-}
-
-/** The session id behind a refresh cookie, for endpoints that need to know
- * which of the caller's sessions is the current one. Returns undefined for
- * a missing/unverifiable cookie rather than throwing β "which session am
- * I" is never the reason a request should fail. */
-export function sessionIdFromRefreshToken(refreshToken: string | undefined): string | undefined {
- if (!refreshToken) return undefined;
- try {
- return verifyRefreshToken(refreshToken).sid;
- } catch {
- return undefined;
- }
-}
-
-/**
- * Guards every authenticated request (Phase 8, issue #39). The access token
- * is otherwise stateless, so a deactivated account's token would keep
- * working for up to 15 minutes; this is the DB read that closes that gap.
- *
- * Deliberately narrow: it answers "is this account still active, and was
- * this token issued after the last deactivation/reactivation", not "is this
- * token on a revocation list" β a general revocation list is issue #18 and
- * is still unbuilt. Because it's a per-request read, tokens carrying no
- * `iat` are treated as revoked once `sessionsInvalidatedAt` is set (fail
- * closed).
- */
-export async function assertActiveSession(userId: string, issuedAtSeconds?: number): Promise {
- const user = await findUserById(userId);
- if (!user) throw new HttpError(401, 'Invalid or expired access token.');
- if (user.deactivatedAt) throw new HttpError(401, 'This account has been deactivated.');
- if (isSessionRevoked(user.sessionsInvalidatedAt, issuedAtSeconds)) {
- throw new HttpError(401, 'Your session has been revoked. Please sign in again.');
- }
-}
-
-function isSessionRevoked(sessionsInvalidatedAt: Date | null, issuedAtSeconds: number | undefined): boolean {
- if (!sessionsInvalidatedAt) return false;
- // `iat` has second precision, so a token minted in the same second as the
- // invalidation counts as revoked β the safe direction.
- return (issuedAtSeconds ?? 0) * 1000 < sessionsInvalidatedAt.getTime();
-}
-
-/** This module's contribution to the patient data export (issue #40): the
- * login identity itself. Explicit allowlist β never the password hash, and
- * no verification/throttle bookkeeping. */
-export const accountExportSection: PatientExportSection = {
- name: 'account',
- version: 1,
- async *pages(subject) {
- const user = await findUserById(subject.userId);
- if (!user) return;
- yield [
- {
- id: user.id,
- email: user.email,
- phone: user.phone,
- role: user.role,
- emailVerifiedAt: user.emailVerifiedAt,
- createdAt: user.createdAt,
- updatedAt: user.updatedAt,
- },
- ];
- },
-};
-// --- Account deletion (Phase 8, issue #41, docs/adr/0010) ----------------
-// The patients module orchestrates `DELETE /patients/me`; this module owns
-// everything about the login identity it touches.
-
-/**
- * Re-confirms the caller's password for a destructive action. Shares the
- * login throttle's per-account bucket: a locked account is refused before
- * the hash is compared, and a wrong password counts as a failed login β so
- * a stolen access token can't be used to brute-force the password here.
- */
-export async function confirmPassword(userId: string, password: string, ip?: string | null): Promise {
- const user = await findUserById(userId);
- if (!user || user.deletedAt) throw new HttpError(401, 'Authentication required.');
-
- const throttleKeys = { emailKey: emailThrottleKey(user.email), ipKey: normalizeClientIp(ip) };
- const gate = await checkLoginThrottle(throttleKeys);
- if (gate.blocked) {
- const afterFailure = await registerFailedLogin(throttleKeys);
- throw new LoginThrottledError(Math.max(gate.retryAfterSeconds, afterFailure.retryAfterSeconds));
- }
-
- if (!(await verifyPassword(password, user.passwordHash))) {
- const afterFailure = await registerFailedLogin(throttleKeys);
- if (afterFailure.blocked) throw new LoginThrottledError(afterFailure.retryAfterSeconds);
- throw new HttpError(403, 'Password confirmation failed.', 'PASSWORD_CONFIRMATION_FAILED');
- }
-}
-
-export async function isAccountDeleted(userId: string): Promise {
- const user = await findUserById(userId);
- return Boolean(user?.deletedAt);
-}
-
-/** The address a deleted account is left with: unique (derived from the id),
- * stable across retries, and undeliverable (`.invalid` is reserved, RFC
- * 2606). It frees the original address for a new signup. */
-export function tombstoneEmail(userId: string): string {
- return `deleted+${userId}@deleted.invalid`;
-}
-
-/** A real bcrypt hash of 32 random bytes nobody ever sees β so the deleted
- * account's login can never succeed, and the login path still pays a
- * normal bcrypt comparison. Computed outside the deletion transaction
- * (bcrypt is deliberately slow). */
-export function createUnusablePasswordHash(): Promise {
- return hashPassword(randomBytes(32).toString('base64url'));
-}
-
-/**
- * Anonymises the login identity, inside the caller's transaction: email to
- * its tombstone, phone cleared, password unusable, verification cleared,
- * `deletedAt` set; verification tokens and the old address's login-throttle
- * bucket deleted. Returns false (and changes nothing) if the account was
- * already deleted β repeat and concurrent deletions are no-ops.
- */
-export async function anonymizeAccount(
- tx: Prisma.TransactionClient,
- userId: string,
- input: { unusablePasswordHash: string; deletedAt: Date },
-): Promise {
- const user = await findUserById(userId, tx);
- if (!user || user.deletedAt) return false;
- const anonymized = await anonymizeUser(tx, userId, {
- tombstoneEmail: tombstoneEmail(userId),
- unusablePasswordHash: input.unusablePasswordHash,
- deletedAt: input.deletedAt,
- });
- if (!anonymized) return false;
- await deleteVerificationTokensForUser(tx, userId);
- await deleteLoginAttemptBucket(tx, 'email', emailThrottleKey(user.email));
- return true;
-}
-
-/** Tombstone purge: removes an already-anonymised User row. */
-export function purgeAnonymizedAccount(tx: Prisma.TransactionClient, userId: string): Promise {
- return deleteAnonymizedUser(tx, userId);
-}
+// Auth services β the only exported surface other modules (and this
+// module's own controllers) may call. Never import repositories/* from
+// outside this module.
+import { config } from '../../../config';
+import { HttpError } from '../../../http-error';
+import {
+ anonymizeUser,
+ consumeVerificationToken,
+ createSelfServeUser,
+ deleteAnonymizedUser,
+ deleteLoginAttemptBucket,
+ deleteVerificationTokensForUser,
+ findLatestVerificationToken,
+ findUserByEmail,
+ findUserById,
+ findVerificationTokenByHash,
+ markUserEmailVerified,
+ replaceVerificationToken,
+} from '../repositories';
+import { hashPassword, verifyPassword } from './password';
+import { signAccessToken, signRefreshToken, verifyRefreshToken } from './tokens';
+import { assertSessionUsable, recordSessionUse, startSession, type SessionContext } from './sessions';
+import { generateVerificationToken, hashVerificationToken } from './verification-tokens';
+import { sendEmail } from './email-dispatcher';
+import {
+ LoginThrottledError,
+ checkLoginThrottle,
+ emailThrottleKey,
+ normalizeClientIp,
+ registerFailedLogin,
+ resetLoginThrottle,
+} from './login-throttle';
+import { csrfTokensMatch, generateCsrfToken } from '../../../middleware/csrf';
+import { redeemAdminInvite } from '../../users/services';
+import type { PatientExportSection } from '../../patients/services';
+import type { Locale } from '../../../i18n';
+import { formatMessage, resolveLocale } from '../../../i18n';
+import { randomBytes } from 'node:crypto';
+import type { Prisma, Role } from '@prisma/client';
+
+// Re-exported so middleware/auth.ts (shared infra, not a peer module) can
+// verify tokens through this module's service surface rather than
+// reaching into services/tokens.ts directly.
+export { verifyAccessToken } from './tokens';
+export type { AccessTokenPayload } from './tokens';
+
+// Password reset (Phase 8 #33) lives in its own file; re-exported here so
+// controllers keep calling a single service surface.
+export {
+ confirmPasswordReset,
+ hashResetToken,
+ requestPasswordReset,
+ resetPasswordResetRateLimits,
+ type ConfirmPasswordResetInput,
+ type RequestPasswordResetInput,
+} from './password-reset';
+
+// Hashing is auth's business (issue #14): the admin-only facility-creation
+// path in the facilities module hashes the initial password through this
+// service surface rather than depending on bcrypt directly.
+export { hashPassword, verifyPassword } from './password';
+
+export {
+ assertSessionUsable,
+ listSessions,
+ recordSessionUse,
+ revokeAllUserSessions,
+ revokeOtherSessions,
+ revokeSession,
+ startSession,
+ type SessionContext,
+ type SessionSummary,
+} from './sessions';
+
+// Re-exported for the same reason: tests (and, at Phase 6, the provider
+// integration) replace the outbound email sender through this module's
+// service surface, not by importing the dispatcher file directly.
+export { setEmailSender, resetEmailSender } from './email-dispatcher';
+export type { EmailMessage, EmailSender } from './email-dispatcher';
+// Login throttling (Phase 8, issue #35) β the login controller needs the
+// error type to set a `Retry-After` header, and src/server.ts runs the
+// cleanup job. Both go through this module's service surface.
+export { LoginThrottledError, clearLoginAttempts, pruneLoginAttempts } from './login-throttle';
+export type { ThrottleDecision } from './login-throttle';
+
+export interface AuthenticatedUser {
+ id: string;
+ email: string;
+ role: Role;
+ locale: string;
+ // Phase 8, issue #34 β exposed so docforum-web can gate booking UI on it
+ // without a second round trip. The server-side guard is the real
+ // enforcement point (`assertEmailVerified`).
+ emailVerifiedAt: Date | null;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+export interface AuthSession {
+ user: AuthenticatedUser;
+ accessToken: string;
+ refreshToken: string;
+ /** Phase 8, issue #37 β handed to the client in the response body so it can
+ * echo it back in the `X-CSRF-Token` header on cookie-authenticated routes.
+ * It deliberately is NOT a JS-readable cookie: docforum-web is a different
+ * site from this API, so it couldn't read one. */
+ csrfToken: string;
+ /** The refresh-session id this pair belongs to (Phase 8 #38) β lets the
+ * client mark its own entry in `GET /auth/sessions` as "this device". */
+ sessionId: string;
+}
+
+function toPublicUser(user: {
+ id: string;
+ email: string;
+ role: Role;
+ locale?: string;
+ emailVerifiedAt: Date | null;
+ createdAt: Date;
+ updatedAt: Date;
+}): AuthenticatedUser {
+ return {
+ id: user.id,
+ email: user.email,
+ role: user.role,
+ locale: user.locale ?? 'en',
+ emailVerifiedAt: user.emailVerifiedAt,
+ createdAt: user.createdAt,
+ updatedAt: user.updatedAt,
+ };
+}
+
+function issueTokensForSession(
+ user: { id: string; role: Role; tokenVersion?: number; locale?: string },
+ sessionId: string,
+): { accessToken: string; refreshToken: string; csrfToken: string } {
+ const payload = { sub: user.id, role: user.role, tv: user.tokenVersion ?? 0, locale: user.locale ?? 'en' };
+ // A fresh CSRF token per session, and per rotation β bound into the
+ // refresh token so verifying it needs no server-side state (issue #37).
+ const csrfToken = generateCsrfToken();
+ // `sid` rides on the refresh token only: access tokens stay stateless so
+ // no protected request needs a session lookup (issue #38's "no write
+ // hot-spot" requirement).
+ return {
+ accessToken: signAccessToken(payload),
+ refreshToken: signRefreshToken({ ...payload, csrf: csrfToken, sid: sessionId }),
+ csrfToken,
+ };
+}
+
+/** Starts a new session (one per login) and issues its first token pair. */
+async function issueNewSession(
+ user: { id: string; role: Role; tokenVersion?: number; locale?: string },
+ context: SessionContext,
+): Promise<{ accessToken: string; refreshToken: string; csrfToken: string; sessionId: string }> {
+ const session = await startSession({ userId: user.id, ...context });
+ return { ...issueTokensForSession(user, session.id), sessionId: session.id };
+}
+
+export interface SignupInput {
+ role: Extract;
+ email: string;
+ password: string;
+ fullName: string;
+ locale?: Locale;
+}
+
+export async function signup(input: SignupInput, context: SessionContext = {}): Promise {
+ // Facility accounts are admin-invited only (PRD OQ-2) β enforced here,
+ // not just in docforum-web's UI. `Extract` on
+ // the type already narrows this at compile time; this is the runtime
+ // guard for a request that bypasses the frontend entirely.
+ if (input.role !== 'patient' && input.role !== 'doctor') {
+ throw new HttpError(400, 'Self-serve signup is only available for patient or doctor accounts.');
+ }
+
+ const existing = await findUserByEmail(input.email);
+ if (existing) {
+ throw new HttpError(409, 'An account with this email already exists.');
+ }
+
+ const passwordHash = await hashPassword(input.password);
+ const user = await createSelfServeUser({
+ email: input.email,
+ passwordHash,
+ role: input.role,
+ fullName: input.fullName,
+ locale: input.locale,
+ });
+
+ // Phase 8, issue #34: the account is created and usable for browsing
+ // immediately, but unverified β mutating routes that depend on a
+ // trustworthy contact address are gated by `assertEmailVerified` until
+ // the link is redeemed.
+ await issueEmailVerification(user);
+
+ const tokens = await issueNewSession(user, context);
+ return { user: toPublicUser(user), ...tokens };
+}
+
+/**
+ * Redeems an admin invite and signs the new admin in (Phase 8, issue #39).
+ *
+ * The invite itself belongs to the users module β this module only hashes
+ * the password and issues the session, calling that module's service rather
+ * than its repository (AGENTS.md hard rule 4).
+ */
+export interface SignupWithInviteInput {
+ token: string;
+ email: string;
+ password: string;
+}
+
+export async function signupWithInvite(input: SignupWithInviteInput, context: SessionContext = {}): Promise {
+ const passwordHash = await hashPassword(input.password);
+ const user = await redeemAdminInvite({ token: input.token, email: input.email, passwordHash });
+
+ const tokens = await issueNewSession(user, context);
+ return {
+ user: {
+ id: user.id,
+ email: user.email,
+ role: user.role,
+ locale: user.locale,
+ // No verification email is sent on the invite path β the invite itself
+ // is the admin-vetted proof of address, so this stays null rather than
+ // claiming a verification that never happened.
+ emailVerifiedAt: null,
+ createdAt: user.createdAt,
+ updatedAt: user.updatedAt,
+ },
+ ...tokens,
+ };
+}
+
+export interface LoginInput {
+ email: string;
+ password: string;
+ /** Client IP as Express resolved it (see TRUST_PROXY in config). Optional
+ * so non-HTTP callers/tests can omit it β the IP bucket is then simply
+ * not tracked for that attempt. */
+ ip?: string | null;
+}
+
+/** A real bcrypt hash (of a random string nobody knows) that a login for a
+ * non-existent address is compared against. Without it, an unknown email
+ * returns in ~0ms while a known one spends ~300ms in bcrypt β a timing
+ * side channel that answers "does this account exist?" even though the
+ * response body is identical. */
+const ABSENT_ACCOUNT_HASH = '$2b$12$z8J5gwFm9TiwC.YpeD6yguMkgakFPA8egT1KEKYiaeVkmb3CHRGXu';
+
+export async function login(input: LoginInput, context: SessionContext = {}): Promise {
+ const throttleKeys = {
+ emailKey: emailThrottleKey(input.email),
+ ipKey: normalizeClientIp(input.ip),
+ };
+
+ // Before the password is even looked at: a locked or backing-off bucket is
+ // refused identically for a real and a non-existent address (issue #35).
+ const gate = await checkLoginThrottle(throttleKeys);
+ if (gate.blocked) {
+ // Still counted: backoff starts below the lock threshold, so if gated
+ // attempts weren't recorded the lock could never be reached. These are
+ // failed attempts either way (no credential was accepted).
+ const afterFailure = await registerFailedLogin(throttleKeys);
+ throw new LoginThrottledError(Math.max(gate.retryAfterSeconds, afterFailure.retryAfterSeconds));
+ }
+
+ const user = await findUserByEmail(input.email);
+ // Same message whether the email doesn't exist or the password is wrong β
+ // and the same *cost*, hence the dummy comparison for an absent account.
+ const valid = await verifyPassword(input.password, user?.passwordHash ?? ABSENT_ACCOUNT_HASH);
+ // `deletedAt` (issue #41): a deleted account's password is already
+ // unusable and its address is a tombstone, so this is defence in depth β
+ // and it answers exactly like a wrong password.
+ if (!user || !valid || user.deletedAt) {
+ const afterFailure = await registerFailedLogin(throttleKeys);
+ if (afterFailure.blocked) throw new LoginThrottledError(afterFailure.retryAfterSeconds);
+ throw new HttpError(401, 'Invalid email or password.');
+ }
+
+ await resetLoginThrottle(throttleKeys.emailKey);
+
+ // Checked *after* the password: a deactivated account must not be
+ // distinguishable from a non-existent one without knowing the password
+ // (Phase 8, issue #39).
+ if (user.deactivatedAt) {
+ throw new HttpError(403, 'This account has been deactivated. Contact an administrator.');
+ }
+
+ const tokens = await issueNewSession(user, context);
+ return { user: toPublicUser(user), ...tokens };
+}
+
+// --- Email verification (Phase 8, issue #34) -----------------------------
+
+/** Mints a fresh token, retires any outstanding one, and hands the link to
+ * the email dispatcher. The raw token is never persisted and never logged. */
+async function issueEmailVerification(user: { id: string; email: string; locale?: string }): Promise {
+ const token = generateVerificationToken();
+ const expiresAt = new Date(Date.now() + config.auth.emailVerification.tokenTtlMinutes * 60_000);
+
+ await replaceVerificationToken({ userId: user.id, tokenHash: hashVerificationToken(token), expiresAt });
+
+ const link = `${config.auth.emailVerification.landingUrl}?token=${encodeURIComponent(token)}`;
+ const locale = resolveLocale(user.locale);
+ const minutes = config.auth.emailVerification.tokenTtlMinutes;
+ try {
+ await sendEmail({
+ to: user.email,
+ subject: formatMessage(locale, 'notifications.emailVerification.subject'),
+ text: formatMessage(locale, 'notifications.emailVerification.body', { minutes, link }),
+ });
+ } catch (err) {
+ // A provider hiccup must not fail signup β the account exists and is
+ // usable, and the user can hit resend. Logged without the message body.
+ // eslint-disable-next-line no-console
+ console.error('[auth] verification email dispatch failed:', err instanceof Error ? err.message : err);
+ }
+}
+
+export interface VerifyEmailResult {
+ emailVerifiedAt: Date;
+}
+
+export async function verifyEmail(rawToken: string): Promise {
+ const record = await findVerificationTokenByHash(hashVerificationToken(rawToken));
+ if (!record) {
+ throw new HttpError(400, 'Invalid email verification token.', 'INVALID_VERIFICATION_TOKEN');
+ }
+ if (record.consumedAt) {
+ throw new HttpError(
+ 400,
+ 'This verification link has already been used. Request a new one if your email is still unverified.',
+ 'INVALID_VERIFICATION_TOKEN',
+ );
+ }
+ if (record.expiresAt.getTime() <= Date.now()) {
+ throw new HttpError(400, 'This verification link has expired. Request a new one.', 'VERIFICATION_TOKEN_EXPIRED');
+ }
+
+ const now = new Date();
+ // Conditional, not read-then-write β two tabs redeeming the same link
+ // must not both report success.
+ const consumed = await consumeVerificationToken(record.id, now);
+ if (!consumed) {
+ throw new HttpError(400, 'This verification link has already been used.', 'INVALID_VERIFICATION_TOKEN');
+ }
+
+ const user = await markUserEmailVerified(record.userId, now);
+ return { emailVerifiedAt: user.emailVerifiedAt ?? now };
+}
+
+export interface ResendVerificationResult {
+ resendAfterSeconds: number;
+}
+
+/** Rate-limited so an unverified account can't be used to flood an inbox.
+ * Account-scoped rather than IP-scoped on purpose, and the endpoint is
+ * authenticated, so it can't be used to probe which addresses exist. */
+export async function resendVerificationEmail(userId: string): Promise {
+ const user = await findUserById(userId);
+ if (!user) throw new HttpError(401, 'Authentication required.');
+
+ if (user.emailVerifiedAt) {
+ throw new HttpError(409, 'This email address is already verified.', 'EMAIL_ALREADY_VERIFIED');
+ }
+
+ const intervalSeconds = config.auth.emailVerification.resendIntervalSeconds;
+ const latest = await findLatestVerificationToken(userId);
+ if (latest) {
+ const waitedSeconds = (Date.now() - latest.createdAt.getTime()) / 1000;
+ if (waitedSeconds < intervalSeconds) {
+ throw new HttpError(
+ 429,
+ `Please wait ${Math.ceil(intervalSeconds - waitedSeconds)}s before requesting another verification email.`,
+ 'EMAIL_VERIFICATION_RATE_LIMITED',
+ );
+ }
+ }
+
+ await issueEmailVerification(user);
+ return { resendAfterSeconds: intervalSeconds };
+}
+
+/** The enforcement point for "unverified accounts can log in and browse, but
+ * not book or pay" (issue #34). Deliberately a DB read rather than a claim
+ * baked into the access token: verifying in another tab then takes effect
+ * immediately instead of after the token's 15-minute TTL. */
+export async function assertEmailVerified(userId: string): Promise {
+ const user = await findUserById(userId);
+ if (!user) throw new HttpError(401, 'Authentication required.');
+ if (!user.emailVerifiedAt) {
+ throw new HttpError(403, 'Verify your email address before continuing.', 'EMAIL_NOT_VERIFIED');
+ }
+}
+
+/**
+ * Exchanges the refresh cookie for a new session.
+ *
+ * `csrfToken` is the `X-CSRF-Token` header value (Phase 8, issue #37),
+ * checked against the claim inside the presented refresh token. The cookie
+ * is verified first so a garbage/missing cookie is still a 401, and a
+ * *valid* cookie without a matching CSRF token is a 403.
+ */
+export async function refresh(refreshToken: string, csrfToken?: string | null): Promise {
+ let payload;
+ try {
+ payload = verifyRefreshToken(refreshToken);
+ } catch {
+ throw new HttpError(401, 'Invalid or expired refresh token.');
+ }
+
+ if (!payload.csrf || !csrfToken || !csrfTokensMatch(csrfToken, payload.csrf)) {
+ throw new HttpError(
+ 403,
+ 'Missing or invalid CSRF token. Send the csrfToken from your login response in the X-CSRF-Token header.',
+ );
+ }
+
+ const user = await findUserById(payload.sub);
+ // A deleted account (issue #41) can't refresh its way back in.
+ if (!user || user.deletedAt) throw new HttpError(401, 'Invalid or expired refresh token.');
+
+ // Deactivated accounts can't refresh out of their deactivation, and a
+ // session revoked by deactivation/reactivation can't come back to life
+ // through a refresh cookie either (Phase 8, issue #39).
+ if (user.deactivatedAt) throw new HttpError(403, 'This account has been deactivated.');
+
+ // `tv` check: a password reset (or anything else that bumps
+ // `User.tokenVersion`) kills every refresh token issued before it. Tokens
+ // predating this field carry no `tv` and read as version 0, which is
+ // exactly right β they stay valid only while the user has never had a
+ // version bump.
+ if ((payload.tv ?? 0) !== user.tokenVersion) {
+ throw new HttpError(401, 'Invalid or expired refresh token.');
+ }
+
+ if (isSessionRevoked(user.sessionsInvalidatedAt, payload.iat)) {
+ throw new HttpError(401, 'Your session has been revoked. Please sign in again.');
+ }
+
+ // Tokens minted before session records existed carry no `sid` and are
+ // rejected: the migration logs existing sessions out once, which is the
+ // honest trade for actually having a revocation store.
+ if (!payload.sid) throw new HttpError(401, 'Invalid or expired refresh token.');
+
+ await assertSessionUsable({ sessionId: payload.sid, userId: user.id });
+ await recordSessionUse(payload.sid);
+
+ // Rotation keeps the same session (same family, same `sid`) β a new
+ // session row per refresh would make "list my sessions" useless.
+ const tokens = issueTokensForSession(user, payload.sid);
+ return { user: toPublicUser(user), ...tokens, sessionId: payload.sid };
+}
+
+/** The session id behind a refresh cookie, for endpoints that need to know
+ * which of the caller's sessions is the current one. Returns undefined for
+ * a missing/unverifiable cookie rather than throwing β "which session am
+ * I" is never the reason a request should fail. */
+export function sessionIdFromRefreshToken(refreshToken: string | undefined): string | undefined {
+ if (!refreshToken) return undefined;
+ try {
+ return verifyRefreshToken(refreshToken).sid;
+ } catch {
+ return undefined;
+ }
+}
+
+/**
+ * Guards every authenticated request (Phase 8, issue #39). The access token
+ * is otherwise stateless, so a deactivated account's token would keep
+ * working for up to 15 minutes; this is the DB read that closes that gap.
+ *
+ * Deliberately narrow: it answers "is this account still active, and was
+ * this token issued after the last deactivation/reactivation", not "is this
+ * token on a revocation list" β a general revocation list is issue #18 and
+ * is still unbuilt. Because it's a per-request read, tokens carrying no
+ * `iat` are treated as revoked once `sessionsInvalidatedAt` is set (fail
+ * closed).
+ */
+export async function assertActiveSession(userId: string, issuedAtSeconds?: number): Promise {
+ const user = await findUserById(userId);
+ if (!user) throw new HttpError(401, 'Invalid or expired access token.');
+ if (user.deactivatedAt) throw new HttpError(401, 'This account has been deactivated.');
+ if (isSessionRevoked(user.sessionsInvalidatedAt, issuedAtSeconds)) {
+ throw new HttpError(401, 'Your session has been revoked. Please sign in again.');
+ }
+}
+
+function isSessionRevoked(sessionsInvalidatedAt: Date | null, issuedAtSeconds: number | undefined): boolean {
+ if (!sessionsInvalidatedAt) return false;
+ // `iat` has second precision, so a token minted in the same second as the
+ // invalidation counts as revoked β the safe direction.
+ return (issuedAtSeconds ?? 0) * 1000 < sessionsInvalidatedAt.getTime();
+}
+
+/** This module's contribution to the patient data export (issue #40): the
+ * login identity itself. Explicit allowlist β never the password hash, and
+ * no verification/throttle bookkeeping. */
+export const accountExportSection: PatientExportSection = {
+ name: 'account',
+ version: 1,
+ async *pages(subject) {
+ const user = await findUserById(subject.userId);
+ if (!user) return;
+ yield [
+ {
+ id: user.id,
+ email: user.email,
+ phone: user.phone,
+ role: user.role,
+ emailVerifiedAt: user.emailVerifiedAt,
+ createdAt: user.createdAt,
+ updatedAt: user.updatedAt,
+ },
+ ];
+ },
+};
+// --- Account deletion (Phase 8, issue #41, docs/adr/0010) ----------------
+// The patients module orchestrates `DELETE /patients/me`; this module owns
+// everything about the login identity it touches.
+
+/**
+ * Re-confirms the caller's password for a destructive action. Shares the
+ * login throttle's per-account bucket: a locked account is refused before
+ * the hash is compared, and a wrong password counts as a failed login β so
+ * a stolen access token can't be used to brute-force the password here.
+ */
+export async function confirmPassword(userId: string, password: string, ip?: string | null): Promise {
+ const user = await findUserById(userId);
+ if (!user || user.deletedAt) throw new HttpError(401, 'Authentication required.');
+
+ const throttleKeys = { emailKey: emailThrottleKey(user.email), ipKey: normalizeClientIp(ip) };
+ const gate = await checkLoginThrottle(throttleKeys);
+ if (gate.blocked) {
+ const afterFailure = await registerFailedLogin(throttleKeys);
+ throw new LoginThrottledError(Math.max(gate.retryAfterSeconds, afterFailure.retryAfterSeconds));
+ }
+
+ if (!(await verifyPassword(password, user.passwordHash))) {
+ const afterFailure = await registerFailedLogin(throttleKeys);
+ if (afterFailure.blocked) throw new LoginThrottledError(afterFailure.retryAfterSeconds);
+ throw new HttpError(403, 'Password confirmation failed.', 'PASSWORD_CONFIRMATION_FAILED');
+ }
+}
+
+export async function isAccountDeleted(userId: string): Promise {
+ const user = await findUserById(userId);
+ return Boolean(user?.deletedAt);
+}
+
+/** The address a deleted account is left with: unique (derived from the id),
+ * stable across retries, and undeliverable (`.invalid` is reserved, RFC
+ * 2606). It frees the original address for a new signup. */
+export function tombstoneEmail(userId: string): string {
+ return `deleted+${userId}@deleted.invalid`;
+}
+
+/** A real bcrypt hash of 32 random bytes nobody ever sees β so the deleted
+ * account's login can never succeed, and the login path still pays a
+ * normal bcrypt comparison. Computed outside the deletion transaction
+ * (bcrypt is deliberately slow). */
+export function createUnusablePasswordHash(): Promise {
+ return hashPassword(randomBytes(32).toString('base64url'));
+}
+
+/**
+ * Anonymises the login identity, inside the caller's transaction: email to
+ * its tombstone, phone cleared, password unusable, verification cleared,
+ * `deletedAt` set; verification tokens and the old address's login-throttle
+ * bucket deleted. Returns false (and changes nothing) if the account was
+ * already deleted β repeat and concurrent deletions are no-ops.
+ */
+export async function anonymizeAccount(
+ tx: Prisma.TransactionClient,
+ userId: string,
+ input: { unusablePasswordHash: string; deletedAt: Date },
+): Promise {
+ const user = await findUserById(userId, tx);
+ if (!user || user.deletedAt) return false;
+ const anonymized = await anonymizeUser(tx, userId, {
+ tombstoneEmail: tombstoneEmail(userId),
+ unusablePasswordHash: input.unusablePasswordHash,
+ deletedAt: input.deletedAt,
+ });
+ if (!anonymized) return false;
+ await deleteVerificationTokensForUser(tx, userId);
+ await deleteLoginAttemptBucket(tx, 'email', emailThrottleKey(user.email));
+ return true;
+}
+
+/** Tombstone purge: removes an already-anonymised User row. */
+export function purgeAnonymizedAccount(tx: Prisma.TransactionClient, userId: string): Promise {
+ return deleteAnonymizedUser(tx, userId);
+}
diff --git a/backend/src/modules/auth/services/password-reset.ts b/backend/src/modules/auth/services/password-reset.ts
index 8f7f2ca8..18b4df16 100644
--- a/backend/src/modules/auth/services/password-reset.ts
+++ b/backend/src/modules/auth/services/password-reset.ts
@@ -1,110 +1,110 @@
-// Password reset flow (issue #33). Two endpoints, deliberately paired:
-// - request: always behaves the same whether or not the email exists.
-// - confirm: single-use, expiring, hashed token β new password + every
-// outstanding refresh token invalidated.
-//
-// Security notes enforced here, not just in the controller:
-// - The raw token only ever exists in memory (and in the outbound
-// notification). The DB stores its SHA-256 hash.
-// - Unknown/expired/reused/superseded tokens all produce the *same*
-// generic error, and the hash comparison is constant-time.
-// - Rate-limited per email and per IP before any account lookup.
-import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
-import { config } from '../../../config';
-import { HttpError } from '../../../http-error';
-import { sendPasswordResetLink } from '../../notifications/services';
-import { hashPassword } from './password';
-import {
- consumePasswordResetToken,
- createPasswordResetToken,
- findPasswordResetTokenByHash,
- findUserByEmail,
-} from '../repositories';
-import { createRateLimiter } from './rate-limit';
-
-const HOUR_MS = 60 * 60 * 1000;
-
-/** Deliberately 400, not 404/410 β the client must not be able to tell
- * which of "no such token", "expired", "already used" or "superseded by a
- * newer request" happened. */
-const GENERIC_INVALID_TOKEN_MESSAGE =
- 'This password reset link is invalid or has expired. Request a new one.';
-
-const emailLimiter = createRateLimiter(config.passwordReset.requestsPerEmailPerHour, HOUR_MS);
-const ipLimiter = createRateLimiter(config.passwordReset.requestsPerIpPerHour, HOUR_MS);
-
-export function hashResetToken(rawToken: string): string {
- return createHash('sha256').update(rawToken).digest('hex');
-}
-
-/** Constant-time comparison of two hex-encoded hashes. */
-function hashesEqual(a: string, b: string): boolean {
- const bufA = Buffer.from(a, 'utf8');
- const bufB = Buffer.from(b, 'utf8');
- if (bufA.length !== bufB.length) return false;
- return timingSafeEqual(bufA, bufB);
-}
-
-export interface RequestPasswordResetInput {
- email: string;
- /** Coarse source address for the per-IP limit. Never persisted. */
- ip: string;
-}
-
-/**
- * Issues a reset token if the email belongs to an account. Always resolves
- * β the caller responds 202 either way, so response *and* status are
- * identical for known and unknown addresses.
- */
-export async function requestPasswordReset(input: RequestPasswordResetInput): Promise {
- if (!emailLimiter.consume(input.email.toLowerCase()) || !ipLimiter.consume(input.ip)) {
- throw new HttpError(429, 'Too many password reset requests. Please try again later.');
- }
-
- const user = await findUserByEmail(input.email);
- if (!user) {
- // Do the same shape of work for an unknown address so timing doesn't
- // leak existence: hash a throwaway token, then drop it.
- hashResetToken(randomBytes(32).toString('base64url'));
- return;
- }
-
- const rawToken = randomBytes(32).toString('base64url');
- const expiresAt = new Date(Date.now() + config.passwordReset.tokenTtlMinutes * 60_000);
-
- await createPasswordResetToken({ userId: user.id, tokenHash: hashResetToken(rawToken), expiresAt });
- await sendPasswordResetLink({ email: user.email, token: rawToken, expiresAt, locale: user.locale });
-}
-
-export interface ConfirmPasswordResetInput {
- token: string;
- newPassword: string;
-}
-
-/** Applies a new password for a valid, unexpired, unused token. */
-export async function confirmPasswordReset(input: ConfirmPasswordResetInput): Promise {
- const presentedHash = hashResetToken(input.token);
- const record = await findPasswordResetTokenByHash(presentedHash);
-
- if (!record || !hashesEqual(record.tokenHash, presentedHash)) {
- throw new HttpError(400, GENERIC_INVALID_TOKEN_MESSAGE);
- }
- if (record.usedAt) throw new HttpError(400, GENERIC_INVALID_TOKEN_MESSAGE);
- if (record.expiresAt.getTime() <= Date.now()) throw new HttpError(400, GENERIC_INVALID_TOKEN_MESSAGE);
-
- const passwordHash = await hashPassword(input.newPassword);
- const consumed = await consumePasswordResetToken({
- tokenId: record.id,
- userId: record.userId,
- passwordHash,
- });
- // Lost the race to a concurrent confirm of the same token.
- if (!consumed) throw new HttpError(400, GENERIC_INVALID_TOKEN_MESSAGE);
-}
-
-/** Test seam only β the limiters are module-level singletons so the limits
- * actually hold across requests. */
-export function resetPasswordResetRateLimits(): void {
- emailLimiter.reset();
- ipLimiter.reset();
-}
+// Password reset flow (issue #33). Two endpoints, deliberately paired:
+// - request: always behaves the same whether or not the email exists.
+// - confirm: single-use, expiring, hashed token β new password + every
+// outstanding refresh token invalidated.
+//
+// Security notes enforced here, not just in the controller:
+// - The raw token only ever exists in memory (and in the outbound
+// notification). The DB stores its SHA-256 hash.
+// - Unknown/expired/reused/superseded tokens all produce the *same*
+// generic error, and the hash comparison is constant-time.
+// - Rate-limited per email and per IP before any account lookup.
+import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
+import { config } from '../../../config';
+import { HttpError } from '../../../http-error';
+import { sendPasswordResetLink } from '../../notifications/services';
+import { hashPassword } from './password';
+import {
+ consumePasswordResetToken,
+ createPasswordResetToken,
+ findPasswordResetTokenByHash,
+ findUserByEmail,
+} from '../repositories';
+import { createRateLimiter } from './rate-limit';
+
+const HOUR_MS = 60 * 60 * 1000;
+
+/** Deliberately 400, not 404/410 β the client must not be able to tell
+ * which of "no such token", "expired", "already used" or "superseded by a
+ * newer request" happened. */
+const GENERIC_INVALID_TOKEN_MESSAGE =
+ 'This password reset link is invalid or has expired. Request a new one.';
+
+const emailLimiter = createRateLimiter(config.passwordReset.requestsPerEmailPerHour, HOUR_MS);
+const ipLimiter = createRateLimiter(config.passwordReset.requestsPerIpPerHour, HOUR_MS);
+
+export function hashResetToken(rawToken: string): string {
+ return createHash('sha256').update(rawToken).digest('hex');
+}
+
+/** Constant-time comparison of two hex-encoded hashes. */
+function hashesEqual(a: string, b: string): boolean {
+ const bufA = Buffer.from(a, 'utf8');
+ const bufB = Buffer.from(b, 'utf8');
+ if (bufA.length !== bufB.length) return false;
+ return timingSafeEqual(bufA, bufB);
+}
+
+export interface RequestPasswordResetInput {
+ email: string;
+ /** Coarse source address for the per-IP limit. Never persisted. */
+ ip: string;
+}
+
+/**
+ * Issues a reset token if the email belongs to an account. Always resolves
+ * β the caller responds 202 either way, so response *and* status are
+ * identical for known and unknown addresses.
+ */
+export async function requestPasswordReset(input: RequestPasswordResetInput): Promise {
+ if (!emailLimiter.consume(input.email.toLowerCase()) || !ipLimiter.consume(input.ip)) {
+ throw new HttpError(429, 'Too many password reset requests. Please try again later.');
+ }
+
+ const user = await findUserByEmail(input.email);
+ if (!user) {
+ // Do the same shape of work for an unknown address so timing doesn't
+ // leak existence: hash a throwaway token, then drop it.
+ hashResetToken(randomBytes(32).toString('base64url'));
+ return;
+ }
+
+ const rawToken = randomBytes(32).toString('base64url');
+ const expiresAt = new Date(Date.now() + config.passwordReset.tokenTtlMinutes * 60_000);
+
+ await createPasswordResetToken({ userId: user.id, tokenHash: hashResetToken(rawToken), expiresAt });
+ await sendPasswordResetLink({ email: user.email, token: rawToken, expiresAt, locale: user.locale });
+}
+
+export interface ConfirmPasswordResetInput {
+ token: string;
+ newPassword: string;
+}
+
+/** Applies a new password for a valid, unexpired, unused token. */
+export async function confirmPasswordReset(input: ConfirmPasswordResetInput): Promise {
+ const presentedHash = hashResetToken(input.token);
+ const record = await findPasswordResetTokenByHash(presentedHash);
+
+ if (!record || !hashesEqual(record.tokenHash, presentedHash)) {
+ throw new HttpError(400, GENERIC_INVALID_TOKEN_MESSAGE);
+ }
+ if (record.usedAt) throw new HttpError(400, GENERIC_INVALID_TOKEN_MESSAGE);
+ if (record.expiresAt.getTime() <= Date.now()) throw new HttpError(400, GENERIC_INVALID_TOKEN_MESSAGE);
+
+ const passwordHash = await hashPassword(input.newPassword);
+ const consumed = await consumePasswordResetToken({
+ tokenId: record.id,
+ userId: record.userId,
+ passwordHash,
+ });
+ // Lost the race to a concurrent confirm of the same token.
+ if (!consumed) throw new HttpError(400, GENERIC_INVALID_TOKEN_MESSAGE);
+}
+
+/** Test seam only β the limiters are module-level singletons so the limits
+ * actually hold across requests. */
+export function resetPasswordResetRateLimits(): void {
+ emailLimiter.reset();
+ ipLimiter.reset();
+}
diff --git a/backend/src/modules/auth/services/rate-limit.ts b/backend/src/modules/auth/services/rate-limit.ts
index ba257e93..70a2bb96 100644
--- a/backend/src/modules/auth/services/rate-limit.ts
+++ b/backend/src/modules/auth/services/rate-limit.ts
@@ -1,48 +1,48 @@
-// Fixed-window in-memory rate limiter (auth module β used by the password
-// reset request endpoint today).
-//
-// Caveat, stated rather than hidden: this is per-process state, so it does
-// not survive a restart and does not hold across multiple backend
-// instances. That's adequate for the current single-instance preview
-// deploy (docs/adr/0003) and for making the endpoint un-spammable in a way
-// that's testable; a horizontally-scaled deployment needs a shared store
-// (Redis) β flagged in ARCHITECTURE_ESSENTIALS.md known gaps, not silently
-// assumed away.
-export interface RateLimiter {
- /** Records an attempt for `key`. Returns true if it's allowed,
- * false if the caller is over the limit for the current window. */
- consume(key: string, now?: number): boolean;
- reset(): void;
-}
-
-const DEFAULT_MAX_TRACKED_KEYS = 10_000;
-
-export function createRateLimiter(limit: number, windowMs: number): RateLimiter {
- const buckets = new Map();
-
- return {
- consume(key: string, now = Date.now()): boolean {
- // Opportunistic prune so a long-running process can't grow the map
- // without bound from one-off IPs/emails.
- if (buckets.size > DEFAULT_MAX_TRACKED_KEYS) {
- for (const [k, bucket] of buckets) {
- if (bucket.resetAt <= now) buckets.delete(k);
- }
- }
-
- const bucket = buckets.get(key);
- if (!bucket || bucket.resetAt <= now) {
- buckets.set(key, { count: 1, resetAt: now + windowMs });
- return true;
- }
-
- if (bucket.count >= limit) return false;
- bucket.count += 1;
- return true;
- },
-
- reset() {
- buckets.clear();
- },
- };
-}
+// Fixed-window in-memory rate limiter (auth module β used by the password
+// reset request endpoint today).
+//
+// Caveat, stated rather than hidden: this is per-process state, so it does
+// not survive a restart and does not hold across multiple backend
+// instances. That's adequate for the current single-instance preview
+// deploy (docs/adr/0003) and for making the endpoint un-spammable in a way
+// that's testable; a horizontally-scaled deployment needs a shared store
+// (Redis) β flagged in ARCHITECTURE_ESSENTIALS.md known gaps, not silently
+// assumed away.
+export interface RateLimiter {
+ /** Records an attempt for `key`. Returns true if it's allowed,
+ * false if the caller is over the limit for the current window. */
+ consume(key: string, now?: number): boolean;
+ reset(): void;
+}
+
+const DEFAULT_MAX_TRACKED_KEYS = 10_000;
+
+export function createRateLimiter(limit: number, windowMs: number): RateLimiter {
+ const buckets = new Map();
+
+ return {
+ consume(key: string, now = Date.now()): boolean {
+ // Opportunistic prune so a long-running process can't grow the map
+ // without bound from one-off IPs/emails.
+ if (buckets.size > DEFAULT_MAX_TRACKED_KEYS) {
+ for (const [k, bucket] of buckets) {
+ if (bucket.resetAt <= now) buckets.delete(k);
+ }
+ }
+
+ const bucket = buckets.get(key);
+ if (!bucket || bucket.resetAt <= now) {
+ buckets.set(key, { count: 1, resetAt: now + windowMs });
+ return true;
+ }
+
+ if (bucket.count >= limit) return false;
+ bucket.count += 1;
+ return true;
+ },
+
+ reset() {
+ buckets.clear();
+ },
+ };
+}
diff --git a/backend/src/modules/auth/services/sessions.ts b/backend/src/modules/auth/services/sessions.ts
index 7448a6b5..db4b2114 100644
--- a/backend/src/modules/auth/services/sessions.ts
+++ b/backend/src/modules/auth/services/sessions.ts
@@ -1,102 +1,102 @@
-// Refresh-session management (Phase 8 #38).
-//
-// One row per refresh-token family (i.e. per login), referenced by the
-// refresh JWT's `sid` claim. This is the revocation store #18 asked for:
-// access tokens stay stateless (15-minute TTL, no per-request lookup), but
-// a refresh only succeeds while its session row is present and unrevoked β
-// which is exactly when revocation has to bite, because that's the
-// operation that mints new tokens.
-import { HttpError } from '../../../http-error';
-import {
- createRefreshSession,
- findRefreshSessionById,
- listRefreshSessions,
- revokeRefreshSession,
- revokeRefreshSessionsForUser,
- touchRefreshSession,
-} from '../repositories';
-
-export interface SessionContext {
- /** Coarse, best-effort source address. Never used for authorization. */
- ip?: string;
- userAgent?: string;
-}
-
-export interface SessionSummary {
- id: string;
- createdAt: Date;
- lastUsedAt: Date;
- userAgent: string | null;
- ip: string | null;
- /** True for the session making this request β the UI's "this device". */
- current: boolean;
-}
-
-export function startSession(input: { userId: string } & SessionContext): Promise<{ id: string }> {
- return createRefreshSession(input);
-}
-
-/**
- * Validates a refresh against its session row. Returns the session id to
- * re-issue against, or throws the same generic 401 the caller uses for any
- * unusable refresh token β a revoked session and a garbage token should be
- * indistinguishable from outside.
- */
-export async function assertSessionUsable(input: { sessionId: string; userId: string }): Promise {
- const session = await findRefreshSessionById(input.sessionId);
- if (!session || session.userId !== input.userId || session.revokedAt) {
- throw new HttpError(401, 'Invalid or expired refresh token.');
- }
-}
-
-/** Marks the session as used. Called on refresh only. */
-export function recordSessionUse(sessionId: string): Promise {
- return touchRefreshSession(sessionId);
-}
-
-export async function listSessions(input: { userId: string; currentSessionId?: string }): Promise {
- const sessions = await listRefreshSessions(input.userId);
- return sessions.map((session) => ({
- id: session.id,
- createdAt: session.createdAt,
- lastUsedAt: session.lastUsedAt,
- userAgent: session.userAgent,
- ip: session.ip,
- current: session.id === input.currentSessionId,
- }));
-}
-
-/**
- * Revokes one of the caller's sessions. `userId` is part of the update's
- * WHERE clause, so another user's session id simply updates nothing β the
- * ownership check is the query, not a branch above it.
- */
-export async function revokeSession(input: {
- userId: string;
- sessionId: string;
- currentSessionId?: string;
-}): Promise<{ revoked: boolean; wasCurrent: boolean }> {
- const result = await revokeRefreshSession(input.sessionId, input.userId);
- if (result.count !== 1) return { revoked: false, wasCurrent: false };
- // Revoking your own session has to clear the cookie too, otherwise the
- // browser keeps presenting a refresh token whose session is dead.
- return { revoked: true, wasCurrent: input.sessionId === input.currentSessionId };
-}
-
-/** "Sign out everywhere else": keeps the caller's own session alive. */
-export async function revokeOtherSessions(input: {
- userId: string;
- currentSessionId?: string;
-}): Promise {
- const result = await revokeRefreshSessionsForUser({
- userId: input.userId,
- exceptSessionId: input.currentSessionId,
- });
- return result.count;
-}
-
-/** Admin escape hatch: revoke every session for a user (issue #38). */
-export async function revokeAllUserSessions(userId: string): Promise {
- const result = await revokeRefreshSessionsForUser({ userId });
- return result.count;
-}
+// Refresh-session management (Phase 8 #38).
+//
+// One row per refresh-token family (i.e. per login), referenced by the
+// refresh JWT's `sid` claim. This is the revocation store #18 asked for:
+// access tokens stay stateless (15-minute TTL, no per-request lookup), but
+// a refresh only succeeds while its session row is present and unrevoked β
+// which is exactly when revocation has to bite, because that's the
+// operation that mints new tokens.
+import { HttpError } from '../../../http-error';
+import {
+ createRefreshSession,
+ findRefreshSessionById,
+ listRefreshSessions,
+ revokeRefreshSession,
+ revokeRefreshSessionsForUser,
+ touchRefreshSession,
+} from '../repositories';
+
+export interface SessionContext {
+ /** Coarse, best-effort source address. Never used for authorization. */
+ ip?: string;
+ userAgent?: string;
+}
+
+export interface SessionSummary {
+ id: string;
+ createdAt: Date;
+ lastUsedAt: Date;
+ userAgent: string | null;
+ ip: string | null;
+ /** True for the session making this request β the UI's "this device". */
+ current: boolean;
+}
+
+export function startSession(input: { userId: string } & SessionContext): Promise<{ id: string }> {
+ return createRefreshSession(input);
+}
+
+/**
+ * Validates a refresh against its session row. Returns the session id to
+ * re-issue against, or throws the same generic 401 the caller uses for any
+ * unusable refresh token β a revoked session and a garbage token should be
+ * indistinguishable from outside.
+ */
+export async function assertSessionUsable(input: { sessionId: string; userId: string }): Promise {
+ const session = await findRefreshSessionById(input.sessionId);
+ if (!session || session.userId !== input.userId || session.revokedAt) {
+ throw new HttpError(401, 'Invalid or expired refresh token.');
+ }
+}
+
+/** Marks the session as used. Called on refresh only. */
+export function recordSessionUse(sessionId: string): Promise {
+ return touchRefreshSession(sessionId);
+}
+
+export async function listSessions(input: { userId: string; currentSessionId?: string }): Promise {
+ const sessions = await listRefreshSessions(input.userId);
+ return sessions.map((session) => ({
+ id: session.id,
+ createdAt: session.createdAt,
+ lastUsedAt: session.lastUsedAt,
+ userAgent: session.userAgent,
+ ip: session.ip,
+ current: session.id === input.currentSessionId,
+ }));
+}
+
+/**
+ * Revokes one of the caller's sessions. `userId` is part of the update's
+ * WHERE clause, so another user's session id simply updates nothing β the
+ * ownership check is the query, not a branch above it.
+ */
+export async function revokeSession(input: {
+ userId: string;
+ sessionId: string;
+ currentSessionId?: string;
+}): Promise<{ revoked: boolean; wasCurrent: boolean }> {
+ const result = await revokeRefreshSession(input.sessionId, input.userId);
+ if (result.count !== 1) return { revoked: false, wasCurrent: false };
+ // Revoking your own session has to clear the cookie too, otherwise the
+ // browser keeps presenting a refresh token whose session is dead.
+ return { revoked: true, wasCurrent: input.sessionId === input.currentSessionId };
+}
+
+/** "Sign out everywhere else": keeps the caller's own session alive. */
+export async function revokeOtherSessions(input: {
+ userId: string;
+ currentSessionId?: string;
+}): Promise {
+ const result = await revokeRefreshSessionsForUser({
+ userId: input.userId,
+ exceptSessionId: input.currentSessionId,
+ });
+ return result.count;
+}
+
+/** Admin escape hatch: revoke every session for a user (issue #38). */
+export async function revokeAllUserSessions(userId: string): Promise {
+ const result = await revokeRefreshSessionsForUser({ userId });
+ return result.count;
+}
diff --git a/backend/src/modules/auth/services/tokens.ts b/backend/src/modules/auth/services/tokens.ts
index 5a0c1c0c..d4c3557f 100644
--- a/backend/src/modules/auth/services/tokens.ts
+++ b/backend/src/modules/auth/services/tokens.ts
@@ -1,52 +1,52 @@
-// Token signing/verification. Every token carries `tv` (the user's
-// refresh-token version at issue time) so bumping `User.tokenVersion`
-// invalidates all outstanding tokens without a per-request DB read β the
-// check happens where the token is actually exchanged (refresh).
-import jwt from 'jsonwebtoken';
-import { config } from '../../../config';
-import type { Role } from '@prisma/client';
-
-export interface AccessTokenPayload {
- sub: string; // user id
- role: Role;
- /** Signup preference used to localize request errors without another DB read. */
- locale?: string;
- /** Issued-at, set by `jsonwebtoken` itself. Read by the auth middleware to
- * compare against `User.sessionsInvalidatedAt` (Phase 8, issue #39) β
- * tokens minted before a deactivation must stop working immediately. */
- iat?: number;
- /** Refresh-session id (Phase 8 #38). Present only on refresh tokens β
- * the access token stays stateless so protected requests don't need a
- * session lookup. */
- sid?: string;
- /** User.tokenVersion at issue time. Compared at refresh time (Phase 8 #33). */
- tv?: number;
-}
-
-/**
- * The refresh token additionally carries the session's CSRF token (Phase 8,
- * issue #37). Binding it here rather than storing it means the CSRF check is
- * stateless and rotates with the refresh token itself: a token minted before
- * this change has no `csrf` claim and is therefore rejected by
- * `refresh()` β a pre-existing refresh cookie forces one re-login, which is
- * the honest cost of closing the hole.
- */
-export interface RefreshTokenPayload extends AccessTokenPayload {
- csrf: string;
-}
-
-export function signAccessToken(payload: AccessTokenPayload): string {
- return jwt.sign(payload, config.jwt.accessSecret, { expiresIn: config.jwt.accessTtl });
-}
-
-export function signRefreshToken(payload: RefreshTokenPayload): string {
- return jwt.sign(payload, config.jwt.refreshSecret, { expiresIn: config.jwt.refreshTtl });
-}
-
-export function verifyAccessToken(token: string): AccessTokenPayload {
- return jwt.verify(token, config.jwt.accessSecret) as AccessTokenPayload;
-}
-
-export function verifyRefreshToken(token: string): RefreshTokenPayload {
- return jwt.verify(token, config.jwt.refreshSecret) as RefreshTokenPayload;
-}
+// Token signing/verification. Every token carries `tv` (the user's
+// refresh-token version at issue time) so bumping `User.tokenVersion`
+// invalidates all outstanding tokens without a per-request DB read β the
+// check happens where the token is actually exchanged (refresh).
+import jwt from 'jsonwebtoken';
+import { config } from '../../../config';
+import type { Role } from '@prisma/client';
+
+export interface AccessTokenPayload {
+ sub: string; // user id
+ role: Role;
+ /** Signup preference used to localize request errors without another DB read. */
+ locale?: string;
+ /** Issued-at, set by `jsonwebtoken` itself. Read by the auth middleware to
+ * compare against `User.sessionsInvalidatedAt` (Phase 8, issue #39) β
+ * tokens minted before a deactivation must stop working immediately. */
+ iat?: number;
+ /** Refresh-session id (Phase 8 #38). Present only on refresh tokens β
+ * the access token stays stateless so protected requests don't need a
+ * session lookup. */
+ sid?: string;
+ /** User.tokenVersion at issue time. Compared at refresh time (Phase 8 #33). */
+ tv?: number;
+}
+
+/**
+ * The refresh token additionally carries the session's CSRF token (Phase 8,
+ * issue #37). Binding it here rather than storing it means the CSRF check is
+ * stateless and rotates with the refresh token itself: a token minted before
+ * this change has no `csrf` claim and is therefore rejected by
+ * `refresh()` β a pre-existing refresh cookie forces one re-login, which is
+ * the honest cost of closing the hole.
+ */
+export interface RefreshTokenPayload extends AccessTokenPayload {
+ csrf: string;
+}
+
+export function signAccessToken(payload: AccessTokenPayload): string {
+ return jwt.sign(payload, config.jwt.accessSecret, { expiresIn: config.jwt.accessTtl });
+}
+
+export function signRefreshToken(payload: RefreshTokenPayload): string {
+ return jwt.sign(payload, config.jwt.refreshSecret, { expiresIn: config.jwt.refreshTtl });
+}
+
+export function verifyAccessToken(token: string): AccessTokenPayload {
+ return jwt.verify(token, config.jwt.accessSecret) as AccessTokenPayload;
+}
+
+export function verifyRefreshToken(token: string): RefreshTokenPayload {
+ return jwt.verify(token, config.jwt.refreshSecret) as RefreshTokenPayload;
+}
diff --git a/backend/src/modules/auth/validation.ts b/backend/src/modules/auth/validation.ts
index 4406603a..e6466768 100644
--- a/backend/src/modules/auth/validation.ts
+++ b/backend/src/modules/auth/validation.ts
@@ -1,49 +1,49 @@
-import { z } from 'zod';
-
-export const signupSchema = z.object({
- role: z.enum(['patient', 'doctor']),
- email: z.string().email(),
- password: z.string().min(8),
- fullName: z.string().trim().min(1),
-});
-
-export const loginSchema = z.object({
- email: z.string().email(),
- password: z.string().min(1),
-});
-
-// Phase 8, issue #39 β admin invite redemption. No `fullName`: an admin
-// account has no Patient/Doctor profile row.
-export const signupWithInviteSchema = z.object({
- token: z.string().min(1),
- email: z.string().email(),
- password: z.string().min(8),
-});
-
-// Phase 8, issue #34. No `email` field on the resend body on purpose β the
-// resend endpoint is authenticated and targets the caller's own account,
-// so it can't be used to probe which addresses exist.
-export const verifyEmailSchema = z.object({
- token: z.string().min(1),
-});
-// Phase 8, issue #35 β admin escape hatch for a lock a legitimate user is
-// stuck behind.
-export const clearLoginAttemptsSchema = z
- .object({
- email: z.string().email().optional(),
- ip: z.string().min(1).optional(),
- })
- .refine((value) => Boolean(value.email || value.ip), {
- message: 'Provide an email or an ip to clear.',
- });
-
-// Phase 8, issue #33 β password reset. The confirm schema repeats signup's
-// minimum password length so a reset can't weaken the account.
-export const passwordResetRequestSchema = z.object({
- email: z.string().email(),
-});
-
-export const passwordResetConfirmSchema = z.object({
- token: z.string().min(1),
- newPassword: z.string().min(8),
-});
+import { z } from 'zod';
+
+export const signupSchema = z.object({
+ role: z.enum(['patient', 'doctor']),
+ email: z.string().email(),
+ password: z.string().min(8),
+ fullName: z.string().trim().min(1),
+});
+
+export const loginSchema = z.object({
+ email: z.string().email(),
+ password: z.string().min(1),
+});
+
+// Phase 8, issue #39 β admin invite redemption. No `fullName`: an admin
+// account has no Patient/Doctor profile row.
+export const signupWithInviteSchema = z.object({
+ token: z.string().min(1),
+ email: z.string().email(),
+ password: z.string().min(8),
+});
+
+// Phase 8, issue #34. No `email` field on the resend body on purpose β the
+// resend endpoint is authenticated and targets the caller's own account,
+// so it can't be used to probe which addresses exist.
+export const verifyEmailSchema = z.object({
+ token: z.string().min(1),
+});
+// Phase 8, issue #35 β admin escape hatch for a lock a legitimate user is
+// stuck behind.
+export const clearLoginAttemptsSchema = z
+ .object({
+ email: z.string().email().optional(),
+ ip: z.string().min(1).optional(),
+ })
+ .refine((value) => Boolean(value.email || value.ip), {
+ message: 'Provide an email or an ip to clear.',
+ });
+
+// Phase 8, issue #33 β password reset. The confirm schema repeats signup's
+// minimum password length so a reset can't weaken the account.
+export const passwordResetRequestSchema = z.object({
+ email: z.string().email(),
+});
+
+export const passwordResetConfirmSchema = z.object({
+ token: z.string().min(1),
+ newPassword: z.string().min(8),
+});
diff --git a/backend/src/modules/availability/repositories/index.ts b/backend/src/modules/availability/repositories/index.ts
index 75e51220..ddc692b2 100644
--- a/backend/src/modules/availability/repositories/index.ts
+++ b/backend/src/modules/availability/repositories/index.ts
@@ -1,70 +1,70 @@
-import { prisma } from '../../../db/prisma-client';
-import type { Prisma } from '@prisma/client';
-
-type DbClient = typeof prisma | Prisma.TransactionClient;
-
-export function findOpenSlots(doctorProfileId: string) {
- return prisma.availabilitySlot.findMany({
- where: { doctorProfileId, status: 'open', startTime: { gte: new Date() } },
- orderBy: { startTime: 'asc' },
- });
-}
-
-export function findSlotById(db: DbClient, slotId: string) {
- return db.availabilitySlot.findUnique({ where: { id: slotId } });
-}
-
-export function findSlotTimesByIds(slotIds: string[]) {
- return prisma.availabilitySlot.findMany({
- where: { id: { in: slotIds } },
- select: { id: true, startTime: true, endTime: true },
- });
-}
-
-export function findExistingSlotRanges(doctorProfileId: string, startTime: Date, endTime: Date) {
- return prisma.availabilitySlot.findMany({
- where: { doctorProfileId, startTime: { gte: startTime }, endTime: { lte: endTime } },
- select: { startTime: true, endTime: true },
- });
-}
-
-export function createSlots(rows: { doctorProfileId: string; startTime: Date; endTime: Date }[]) {
- return prisma.availabilitySlot.createMany({ data: rows });
-}
-
-/**
- * THE mechanism that prevents double-booking (ARCHITECTURE.md Β§5.1): a
- * single conditional UPDATE, atomic at the database level regardless of
- * how many requests race to call this for the same slotId. Postgres
- * serializes concurrent UPDATEs to the same row via row-level locking β
- * exactly one caller's WHERE clause matches `status = 'open'`; every other
- * concurrent caller's WHERE clause evaluates against the now-committed
- * `booked` row and matches zero rows.
- *
- * Accepts a transaction client so callers (appointments/services) can run
- * this as one step inside a larger transaction that also creates the
- * Appointment row β see backend/tests/integration/booking.test.ts for the
- * concurrency proof.
- */
-export async function claimOpenSlot(db: DbClient, slotId: string): Promise {
- const result = await db.availabilitySlot.updateMany({
- where: { id: slotId, status: 'open' },
- data: { status: 'booked' },
- });
- return result.count === 1;
-}
-
-/**
- * Inverse of claimOpenSlot, used when a FUTURE booked slot is freed by a
- * cancellation (direct booking or the referral cascade). Conditional on
- * both `booked` and `startTime > now` β a slot whose time has passed is
- * left alone: the visit happened (or didn't) either way, and re-opening
- * it would put unbookable time back on the calendar.
- */
-export async function releaseBookedSlotIfFuture(db: DbClient, slotId: string): Promise {
- const result = await db.availabilitySlot.updateMany({
- where: { id: slotId, status: 'booked', startTime: { gt: new Date() } },
- data: { status: 'open' },
- });
- return result.count === 1;
-}
+import { prisma } from '../../../db/prisma-client';
+import type { Prisma } from '@prisma/client';
+
+type DbClient = typeof prisma | Prisma.TransactionClient;
+
+export function findOpenSlots(doctorProfileId: string) {
+ return prisma.availabilitySlot.findMany({
+ where: { doctorProfileId, status: 'open', startTime: { gte: new Date() } },
+ orderBy: { startTime: 'asc' },
+ });
+}
+
+export function findSlotById(db: DbClient, slotId: string) {
+ return db.availabilitySlot.findUnique({ where: { id: slotId } });
+}
+
+export function findSlotTimesByIds(slotIds: string[]) {
+ return prisma.availabilitySlot.findMany({
+ where: { id: { in: slotIds } },
+ select: { id: true, startTime: true, endTime: true },
+ });
+}
+
+export function findExistingSlotRanges(doctorProfileId: string, startTime: Date, endTime: Date) {
+ return prisma.availabilitySlot.findMany({
+ where: { doctorProfileId, startTime: { gte: startTime }, endTime: { lte: endTime } },
+ select: { startTime: true, endTime: true },
+ });
+}
+
+export function createSlots(rows: { doctorProfileId: string; startTime: Date; endTime: Date }[]) {
+ return prisma.availabilitySlot.createMany({ data: rows });
+}
+
+/**
+ * THE mechanism that prevents double-booking (ARCHITECTURE.md Β§5.1): a
+ * single conditional UPDATE, atomic at the database level regardless of
+ * how many requests race to call this for the same slotId. Postgres
+ * serializes concurrent UPDATEs to the same row via row-level locking β
+ * exactly one caller's WHERE clause matches `status = 'open'`; every other
+ * concurrent caller's WHERE clause evaluates against the now-committed
+ * `booked` row and matches zero rows.
+ *
+ * Accepts a transaction client so callers (appointments/services) can run
+ * this as one step inside a larger transaction that also creates the
+ * Appointment row β see backend/tests/integration/booking.test.ts for the
+ * concurrency proof.
+ */
+export async function claimOpenSlot(db: DbClient, slotId: string): Promise {
+ const result = await db.availabilitySlot.updateMany({
+ where: { id: slotId, status: 'open' },
+ data: { status: 'booked' },
+ });
+ return result.count === 1;
+}
+
+/**
+ * Inverse of claimOpenSlot, used when a FUTURE booked slot is freed by a
+ * cancellation (direct booking or the referral cascade). Conditional on
+ * both `booked` and `startTime > now` β a slot whose time has passed is
+ * left alone: the visit happened (or didn't) either way, and re-opening
+ * it would put unbookable time back on the calendar.
+ */
+export async function releaseBookedSlotIfFuture(db: DbClient, slotId: string): Promise {
+ const result = await db.availabilitySlot.updateMany({
+ where: { id: slotId, status: 'booked', startTime: { gt: new Date() } },
+ data: { status: 'open' },
+ });
+ return result.count === 1;
+}
diff --git a/backend/src/modules/availability/services/index.ts b/backend/src/modules/availability/services/index.ts
index 9d79928a..d70a3966 100644
--- a/backend/src/modules/availability/services/index.ts
+++ b/backend/src/modules/availability/services/index.ts
@@ -1,57 +1,57 @@
-import { HttpError } from '../../../http-error';
-import * as availabilityRepo from '../repositories';
-import * as doctorsService from '../../doctors/services';
-import { buildSlotRanges } from '../slot-generation';
-
-export function listOpenSlots(doctorProfileId: string) {
- return availabilityRepo.findOpenSlots(doctorProfileId);
-}
-
-export interface GenerateSlotsInput {
- doctorProfileId: string;
- /** ISO date, e.g. "2026-09-20" β single day only, no recurrence
- * (ARCHITECTURE.md Β§6.3.6 overengineering guardrail: fixed-length slot
- * generation only, no recurrence/buffer engine). */
- date: string;
- startHour: number; // 0-23, UTC
- endHour: number; // 0-23, UTC, exclusive
- slotLengthMinutes: number;
-}
-
-export async function generateSlots(input: GenerateSlotsInput) {
- const { doctorProfileId, date, startHour, endHour } = input;
- try {
- buildSlotRanges(input);
- } catch (error) {
- throw new HttpError(400, error instanceof Error ? error.message : 'Invalid slot-generation input.');
- }
- const dayStart = new Date(`${date}T00:00:00.000Z`);
- const rangeStart = new Date(dayStart.getTime() + startHour * 60 * 60 * 1_000);
- const rangeEnd = new Date(dayStart.getTime() + endHour * 60 * 60 * 1_000);
- const existing = await availabilityRepo.findExistingSlotRanges(doctorProfileId, rangeStart, rangeEnd);
- const existingStarts = existing.map((slot) => slot.startTime.getTime());
- const rows = buildSlotRanges(input, existingStarts);
-
- if (rows.length === 0) return { created: 0 };
-
- const result = await availabilityRepo.createSlots(rows);
- return { created: result.count };
-}
-
-/** Re-exported for appointments/services β the only cross-module entry
- * points into this module's booking-concurrency primitive, slot reads and
- * slot release. Never import availability/repositories from another
- * module directly. */
-export { claimOpenSlot, findSlotById as getSlot, releaseBookedSlotIfFuture } from '../repositories';
-
-/** Batch slot-time lookup for other modules (patient data export, issue
- * #40). Times only β no status or doctor internals. */
-export async function getSlotTimesByIds(slotIds: string[]): Promise