From cba3f93be995aee9136975076f1657c6108b6e5e Mon Sep 17 00:00:00 2001 From: InsertUzername Date: Mon, 5 Oct 2026 11:57:38 -0700 Subject: [PATCH 01/12] package-lock --- apps/mobile/package-lock.json | 377 +++++++++++++++++++--------------- 1 file changed, 207 insertions(+), 170 deletions(-) diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json index 16c3a30..134defb 100644 --- a/apps/mobile/package-lock.json +++ b/apps/mobile/package-lock.json @@ -1623,6 +1623,71 @@ "metro-transform-worker": "0.84.5" } }, + "node_modules/@expo/metro-config": { + "version": "57.0.12", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-57.0.12.tgz", + "integrity": "sha512-S62Lrq35HZqBFD55423pmWb8PjaiR/W02zQC1uECBmw1vTN8WZaFz4TJ0i21EeJzwfebMb9MLxL8JZ102Z6VbA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.5", + "@expo/config": "~57.0.9", + "@expo/env": "~2.4.3", + "@expo/json-file": "~11.0.1", + "@expo/metro": "~56.0.2", + "@expo/require-utils": "^57.0.5", + "@expo/spawn-async": "^1.8.0", + "@jridgewell/gen-mapping": "^0.3.13", + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/sourcemap-codec": "^1.5.5", + "browserslist": "^4.25.0", + "chalk": "^4.1.0", + "debug": "^4.3.2", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "hermes-parser": "^0.36.0", + "jsc-safe-url": "^0.2.4", + "lightningcss": "^1.30.1", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "expo": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@expo/metro-config/node_modules/@expo/env": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.4.3.tgz", + "integrity": "sha512-M1NXeZCA1mkMkYOyIe7PlyRX0/jqFtMoJgyblnlq/vpCRfmueFT7RnGSQG8uEFDF5WHOFGijAQ3fogPh3/n5Ng==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "debug": "^4.3.4", + "getenv": "^2.0.0" + }, + "engines": { + "node": ">=20.12.0" + } + }, + "node_modules/@expo/metro-config/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@expo/metro-file-map": { "version": "57.0.3", "resolved": "https://registry.npmjs.org/@expo/metro-file-map/-/metro-file-map-57.0.3.tgz", @@ -3554,9 +3619,9 @@ } }, "node_modules/babel-plugin-react-native-web": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.2.tgz", - "integrity": "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==", + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.3.tgz", + "integrity": "sha512-e4fG8NbyRvk5GwU/7G2U9WxzPYtpC4IQN28AyRDzKE0kvywgohq8vT2x0/xBfg8a0RfSkXLlHAtUAGlSy3VhAw==", "license": "MIT" }, "node_modules/babel-plugin-syntax-hermes-parser": { @@ -3604,6 +3669,107 @@ "@babel/core": "^7.0.0 || ^8.0.0-0" } }, + "node_modules/babel-preset-expo": { + "version": "57.0.13", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.13.tgz", + "integrity": "sha512-OOl3lqNQ4IagJfsNgtxuna0wpTWt1BswdyktGTOJ5EJCgdQUk/B2vaj/jsl03+kMyuIvpxKioVtbTzxh3mdvvQ==", + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.20.5", + "@babel/helper-module-imports": "^7.25.9", + "@babel/plugin-proposal-decorators": "^7.12.9", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.28.6", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/preset-typescript": "^7.23.0", + "@react-native/babel-plugin-codegen": "0.86.3", + "babel-plugin-react-compiler": "^1.0.0", + "babel-plugin-react-native-web": "~0.21.0", + "babel-plugin-syntax-hermes-parser": "^0.36.0", + "babel-plugin-transform-flow-enums": "^0.0.2", + "debug": "^4.3.4" + }, + "peerDependencies": { + "@babel/runtime": "^7.20.0", + "expo": "*", + "expo-widgets": "^57.0.21", + "react-refresh": ">=0.14.0 <1.0.0" + }, + "peerDependenciesMeta": { + "@babel/runtime": { + "optional": true + }, + "expo": { + "optional": true + }, + "expo-widgets": { + "optional": true + } + } + }, + "node_modules/babel-preset-expo/node_modules/@react-native/babel-plugin-codegen": { + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.86.3.tgz", + "integrity": "sha512-O6Xza4JBGPIU8J7YbKTyBoYL4thpy8jMW/oaLDWdAyOwYHKIjK47pAL5HUEbOe2bWz2PEKjbYRF2ApkJv1ottQ==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.0", + "@react-native/codegen": "0.86.3" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/babel-preset-expo/node_modules/@react-native/codegen": { + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.86.3.tgz", + "integrity": "sha512-Ux4jHi0fh+bdtVEcL0gaPLbY56V+SvFUDl/8sRAE1jdb4k+o7fT/4Nc29yz4X+qfjstkSqObQTMBGhdzxH9JvA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/parser": "^7.29.0", + "hermes-parser": "0.36.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "tinyglobby": "^0.2.15", + "yargs": "^17.6.2" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, "node_modules/babel-preset-jest": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", @@ -4780,13 +4946,13 @@ "license": "MIT" }, "node_modules/expo": { - "version": "57.0.24", - "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.24.tgz", - "integrity": "sha512-LxSnKMv7en/w4XDNxW3VOx7SbtQ2Bs4MyGhtoZCToDsEsBPO7pJXlv5GnQUCd7Wi8118QV/VDwsDLBKTB3Z3iQ==", + "version": "57.0.26", + "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.26.tgz", + "integrity": "sha512-U1rsFwcuZNnU61Z/YLnnVC/ZqP0wpxh/rCKUMff3ot/rhLpxFY1zxEqu4vyaexyvBAB1mwpl9lA/ltcHBoQGyg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "^57.0.26", + "@expo/cli": "^57.0.27", "@expo/config": "~57.0.9", "@expo/config-plugins": "~57.0.9", "@expo/devtools": "~57.0.1", @@ -4797,14 +4963,14 @@ "@expo/metro": "~56.0.2", "@expo/metro-config": "~57.0.12", "@ungap/structured-clone": "^1.3.0", - "babel-preset-expo": "~57.0.12", + "babel-preset-expo": "~57.0.13", "expo-asset": "~57.0.18", - "expo-constants": "~57.0.19", + "expo-constants": "~57.0.20", "expo-file-system": "~57.0.7", "expo-font": "~57.0.4", "expo-keep-awake": "~57.0.2", "expo-modules-autolinking": "~57.0.13", - "expo-modules-core": "~57.0.18", + "expo-modules-core": "~57.0.20", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.2" @@ -4842,9 +5008,9 @@ } }, "node_modules/expo-constants": { - "version": "57.0.19", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.19.tgz", - "integrity": "sha512-3eK1kZRwCdMizQLlM/PPlPwjaPnB5qKvdRDMBMnB1ZxmvWJBf9TB4uEO4DMMfIPaFUxYFs29dsR5fPr1gh3FTQ==", + "version": "57.0.20", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.20.tgz", + "integrity": "sha512-WycPVvBgbluSa6yAh6DKnX646ZHrae0gV2ZLum5fioNkR+DiWyulq1+UfS++Jc6yfAh/l55NOirv6mt5cnQhSw==", "license": "MIT", "dependencies": { "@expo/env": "~2.4.3" @@ -4955,13 +5121,13 @@ } }, "node_modules/expo-modules-core": { - "version": "57.0.18", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.18.tgz", - "integrity": "sha512-ovB7C+GVQTlKAOIsOIHJVVXN9Mn0ljf2SYBS+afrRaGqq+HZEn1D3o+HReIVeH1NjCn/xDBb7I4HjldHs6luZA==", + "version": "57.0.20", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.20.tgz", + "integrity": "sha512-vEL3aeih32yuD7sQHB3Y3LKXRT8mMgoVQWFIcHtX811Bl0StnNZUG2vT5je4fe+OWYVfkjb6gNqfMc04moREgA==", "license": "MIT", "dependencies": { "@expo/expo-modules-macros-plugin": "0.6.1", - "expo-modules-jsi": "~57.1.0", + "expo-modules-jsi": "~57.1.1", "invariant": "^2.2.4" }, "peerDependencies": { @@ -4976,9 +5142,9 @@ } }, "node_modules/expo-modules-jsi": { - "version": "57.1.0", - "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.1.0.tgz", - "integrity": "sha512-a5ckeHfnbYfonhcHGkM0EU/a0Keh+/OufXy/HyFS+spr5Ib0N4Oh1ZsSYFQhp5xdOUFkVofKRhfH+VmcccIIpA==", + "version": "57.1.1", + "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.1.1.tgz", + "integrity": "sha512-ssnwz3ZuIgCbZj5A3J6NlhQrXZojg+BjkeWrOWqVJhyMHlV/A/cRT7s986chKku97BFt6ucmDu6/BCzrV45Wcw==", "license": "MIT", "peerDependencies": { "react-native": "*" @@ -5278,9 +5444,9 @@ } }, "node_modules/expo/node_modules/@expo/cli": { - "version": "57.0.26", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.26.tgz", - "integrity": "sha512-vm769xuEyHMToi3uY66JLzRA2GGK+3WzdH71HNn67FuVhry82dKTvMUoeHYtHiindyeieiWfEvHT8Kr1VyEVPQ==", + "version": "57.0.27", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.27.tgz", + "integrity": "sha512-Jauk4chxmpVG5ElrMLTCwjFP20jbYU7PB4l3LYV8j+4e/3HU5jdl7qTQ4eVy1AaDUE9Ia2NCQ5A9kar2V1gaLg==", "license": "MIT", "dependencies": { "@expo/code-signing-certificates": "^0.0.6", @@ -5300,7 +5466,7 @@ "@expo/plist": "^0.8.1", "@expo/prebuild-config": "^57.0.16", "@expo/require-utils": "^57.0.5", - "@expo/router-server": "^57.0.10", + "@expo/router-server": "^57.0.11", "@expo/schema-utils": "^57.0.2", "@expo/spawn-async": "^1.8.0", "@expo/ws-tunnel": "^2.0.0", @@ -5361,17 +5527,17 @@ } }, "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": { - "version": "57.0.10", - "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-57.0.10.tgz", - "integrity": "sha512-8rGh6N5di7jzu7UC1SuIA1TWmVd+vBwOHwC7/1ww+FA6CcxcWyQlcPl39h2+Jpnul78CVzWpIjOcdKsZsp2zXw==", + "version": "57.0.11", + "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-57.0.11.tgz", + "integrity": "sha512-0QxbqEkceb1WDeu5VqNdnTPVHlm3U183F7Y1t5LMohmGRAkHVL0PB4tCHBvEqKpLzOzHVObw3reGkJ1tX48h8Q==", "license": "MIT", "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { - "@expo/metro-runtime": "^57.0.15", + "@expo/metro-runtime": "^57.0.16", "expo": "*", - "expo-constants": "^57.0.18", + "expo-constants": "^57.0.19", "expo-font": "^57.0.4", "expo-router": "*", "expo-server": "^57.0.3", @@ -5408,79 +5574,6 @@ "node": ">=20.12.0" } }, - "node_modules/expo/node_modules/@expo/metro-config": { - "version": "57.0.12", - "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-57.0.12.tgz", - "integrity": "sha512-S62Lrq35HZqBFD55423pmWb8PjaiR/W02zQC1uECBmw1vTN8WZaFz4TJ0i21EeJzwfebMb9MLxL8JZ102Z6VbA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.20.0", - "@babel/core": "^7.20.0", - "@babel/generator": "^7.20.5", - "@expo/config": "~57.0.9", - "@expo/env": "~2.4.3", - "@expo/json-file": "~11.0.1", - "@expo/metro": "~56.0.2", - "@expo/require-utils": "^57.0.5", - "@expo/spawn-async": "^1.8.0", - "@jridgewell/gen-mapping": "^0.3.13", - "@jridgewell/remapping": "^2.3.5", - "@jridgewell/sourcemap-codec": "^1.5.5", - "browserslist": "^4.25.0", - "chalk": "^4.1.0", - "debug": "^4.3.2", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "hermes-parser": "^0.36.0", - "jsc-safe-url": "^0.2.4", - "lightningcss": "^1.30.1", - "picomatch": "^4.0.4", - "postcss": "^8.5.14", - "resolve-from": "^5.0.0" - }, - "peerDependencies": { - "expo": "*" - }, - "peerDependenciesMeta": { - "expo": { - "optional": true - } - } - }, - "node_modules/expo/node_modules/@react-native/babel-plugin-codegen": { - "version": "0.86.3", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.86.3.tgz", - "integrity": "sha512-O6Xza4JBGPIU8J7YbKTyBoYL4thpy8jMW/oaLDWdAyOwYHKIjK47pAL5HUEbOe2bWz2PEKjbYRF2ApkJv1ottQ==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.0", - "@react-native/codegen": "0.86.3" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/expo/node_modules/@react-native/codegen": { - "version": "0.86.3", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.86.3.tgz", - "integrity": "sha512-Ux4jHi0fh+bdtVEcL0gaPLbY56V+SvFUDl/8sRAE1jdb4k+o7fT/4Nc29yz4X+qfjstkSqObQTMBGhdzxH9JvA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/parser": "^7.29.0", - "hermes-parser": "0.36.0", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "tinyglobby": "^0.2.15", - "yargs": "^17.6.2" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - }, - "peerDependencies": { - "@babel/core": "*" - } - }, "node_modules/expo/node_modules/@react-native/debugger-frontend": { "version": "0.86.3", "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.86.3.tgz", @@ -5573,73 +5666,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/expo/node_modules/babel-preset-expo": { - "version": "57.0.12", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.12.tgz", - "integrity": "sha512-rrZqJPYbVevy7rqibJZQFd3AsleGslJ63vmB3Qs8b5poOO5AHaehUGbZHjb0HoOZO59d86BEifYbq0/ncoM2UQ==", - "license": "MIT", - "dependencies": { - "@babel/generator": "^7.20.5", - "@babel/helper-module-imports": "^7.25.9", - "@babel/plugin-proposal-decorators": "^7.12.9", - "@babel/plugin-proposal-export-default-from": "^7.24.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-default-from": "^7.24.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-transform-async-generator-functions": "^7.25.4", - "@babel/plugin-transform-async-to-generator": "^7.24.7", - "@babel/plugin-transform-block-scoping": "^7.25.0", - "@babel/plugin-transform-class-properties": "^7.25.4", - "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-classes": "^7.25.4", - "@babel/plugin-transform-destructuring": "^7.24.8", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-flow-strip-types": "^7.25.2", - "@babel/plugin-transform-for-of": "^7.24.7", - "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.8", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", - "@babel/plugin-transform-object-rest-spread": "^7.24.7", - "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.8", - "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.24.7", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-react-display-name": "^7.24.7", - "@babel/plugin-transform-react-jsx": "^7.28.6", - "@babel/plugin-transform-react-jsx-development": "^7.27.1", - "@babel/plugin-transform-react-pure-annotations": "^7.27.1", - "@babel/plugin-transform-runtime": "^7.24.7", - "@babel/plugin-transform-typescript": "^7.25.2", - "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@babel/preset-typescript": "^7.23.0", - "@react-native/babel-plugin-codegen": "0.86.3", - "babel-plugin-react-compiler": "^1.0.0", - "babel-plugin-react-native-web": "~0.21.0", - "babel-plugin-syntax-hermes-parser": "^0.36.0", - "babel-plugin-transform-flow-enums": "^0.0.2", - "debug": "^4.3.4" - }, - "peerDependencies": { - "@babel/runtime": "^7.20.0", - "expo": "*", - "expo-widgets": "^57.0.19", - "react-refresh": ">=0.14.0 <1.0.0" - }, - "peerDependenciesMeta": { - "@babel/runtime": { - "optional": true - }, - "expo": { - "optional": true - }, - "expo-widgets": { - "optional": true - } - } - }, "node_modules/expo/node_modules/expo-asset": { "version": "57.0.18", "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-57.0.18.tgz", @@ -5947,7 +5973,6 @@ "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, @@ -8066,6 +8091,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8086,6 +8114,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8106,6 +8137,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8126,6 +8160,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ From 7f785653759f0a5988698fa65668d568bbdd26b4 Mon Sep 17 00:00:00 2001 From: InsertUzername Date: Tue, 6 Oct 2026 16:16:21 -0700 Subject: [PATCH 02/12] feat(mobile): add session-only account info prototype --- apps/mobile/__tests__/account-info.test.tsx | 78 +++++ apps/mobile/__tests__/auth-flow.test.tsx | 35 ++- apps/mobile/app/account.tsx | 28 ++ apps/mobile/app/home.tsx | 7 +- .../features/account/AccountInfoScreen.tsx | 284 ++++++++++++++++++ apps/mobile/features/account/styles.ts | 60 ++++ apps/mobile/features/account/types.ts | 17 ++ .../features/auth/CreateAccountScreen.tsx | 8 +- apps/mobile/features/home/HomeScreen.tsx | 12 +- apps/mobile/tsconfig.json | 10 +- docs/plans/account-info-page.md | 248 +++++++++++++++ 11 files changed, 768 insertions(+), 19 deletions(-) create mode 100644 apps/mobile/__tests__/account-info.test.tsx create mode 100644 apps/mobile/app/account.tsx create mode 100644 apps/mobile/features/account/AccountInfoScreen.tsx create mode 100644 apps/mobile/features/account/styles.ts create mode 100644 apps/mobile/features/account/types.ts create mode 100644 docs/plans/account-info-page.md diff --git a/apps/mobile/__tests__/account-info.test.tsx b/apps/mobile/__tests__/account-info.test.tsx new file mode 100644 index 0000000..e3e0912 --- /dev/null +++ b/apps/mobile/__tests__/account-info.test.tsx @@ -0,0 +1,78 @@ +import { fireEvent, render, waitFor } from "@testing-library/react-native"; + +import { AccountInfoScreen } from "../features/account/AccountInfoScreen"; +import type { AccountInfo } from "../features/account/types"; + +const account: AccountInfo = { + email: "shopper@example.com", + notificationPreferences: { + priceChange: { email: false, push: false }, + restock: { email: false, push: false }, + }, + pendingEmail: null, + phone: null, + pushAvailability: "unavailable", + username: "quickcop_user", + verifiedEmail: "shopper@example.com", +}; + +describe("AccountInfoScreen", () => { + it("saves editable account details and independent alert preferences", async () => { + const onSave = jest.fn, [AccountInfo]>().mockResolvedValue(); + const screen = await render( + , + ); + + expect(screen.getByDisplayValue("quickcop_user")).toBeTruthy(); + expect( + screen.getByText("Monitoring continues even when every alert is off."), + ).toBeTruthy(); + expect( + screen.getByText("Push notifications are unavailable on this device."), + ).toBeTruthy(); + + fireEvent.changeText(screen.getByLabelText("Username"), "new_name"); + fireEvent.changeText( + screen.getByLabelText("Phone number (optional)"), + "555-0100", + ); + fireEvent( + screen.getByLabelText("Restock push alerts"), + "valueChange", + true, + ); + fireEvent( + screen.getByLabelText("Price-change email alerts"), + "valueChange", + true, + ); + fireEvent.press(screen.getByRole("button", { name: "Save changes" })); + + await waitFor(() => { + expect(onSave).toHaveBeenCalledWith({ + ...account, + notificationPreferences: { + priceChange: { email: true, push: false }, + restock: { email: false, push: true }, + }, + phone: "555-0100", + username: "new_name", + }); + }); + + expect(screen.getByText("Changes saved for this session.")).toBeTruthy(); + }); + + it("requires a username and a valid email before saving", async () => { + const screen = await render( + , + ); + + fireEvent.changeText(screen.getByLabelText("Username"), " "); + fireEvent.changeText(screen.getByLabelText("Email"), "not-an-email"); + fireEvent.press(screen.getByRole("button", { name: "Save changes" })); + + expect(screen.getByText("Enter a username.")).toBeTruthy(); + expect(screen.getByText("Enter a valid email address.")).toBeTruthy(); + }); +}); diff --git a/apps/mobile/__tests__/auth-flow.test.tsx b/apps/mobile/__tests__/auth-flow.test.tsx index c575217..26e49ce 100644 --- a/apps/mobile/__tests__/auth-flow.test.tsx +++ b/apps/mobile/__tests__/auth-flow.test.tsx @@ -5,9 +5,9 @@ import { LoginScreen } from "../features/auth/LoginScreen"; import { HomeScreen } from "../features/home/HomeScreen"; describe("local authentication prototype", () => { - it("opens the home screen after the user submits a username and password", () => { + it("opens the home screen after the user submits a username and password", async () => { const onLogin = jest.fn(); - const screen = render( + const screen = await render( , ); @@ -18,9 +18,9 @@ describe("local authentication prototype", () => { expect(onLogin).toHaveBeenCalledTimes(1); }); - it("allows account creation with an email and no phone number", () => { + it("allows account creation with an email and no phone number", async () => { const onAccountCreated = jest.fn(); - const screen = render( + const screen = await render( { expect(onAccountCreated).toHaveBeenCalledTimes(1); }); - it("returns to login when the user logs out from the empty home screen", () => { + it("requires an email even when a phone number is provided", async () => { + const onAccountCreated = jest.fn(); + const screen = await render( + , + ); + + fireEvent.changeText( + screen.getByLabelText("Phone number (optional)"), + "555-0100", + ); + fireEvent.changeText(screen.getByLabelText("Username"), "quickcop_user"); + fireEvent.changeText(screen.getByLabelText("Password"), "password123"); + fireEvent.press(screen.getByRole("button", { name: "Create account" })); + + expect(onAccountCreated).not.toHaveBeenCalled(); + expect( + screen.getByText("Enter an email, a username, and a password."), + ).toBeTruthy(); + }); + + it("returns to login when the user logs out from the empty home screen", async () => { const onLogout = jest.fn(); - const screen = render(); + const screen = await render(); fireEvent.press(screen.getByRole("button", { name: "Log out" })); diff --git a/apps/mobile/app/account.tsx b/apps/mobile/app/account.tsx new file mode 100644 index 0000000..095ebcf --- /dev/null +++ b/apps/mobile/app/account.tsx @@ -0,0 +1,28 @@ +import { useState } from "react"; + +import { AccountInfoScreen } from "../features/account/AccountInfoScreen"; +import type { AccountInfo } from "../features/account/types"; + +const prototypeAccount: AccountInfo = { + email: "shopper@example.com", + notificationPreferences: { + priceChange: { email: false, push: false }, + restock: { email: false, push: false }, + }, + pendingEmail: null, + phone: null, + pushAvailability: "unavailable", + username: "quickcop_user", + verifiedEmail: "shopper@example.com", +}; + +export default function AccountRoute() { + const [account, setAccount] = useState(prototypeAccount); + + return ( + setAccount(next)} + /> + ); +} diff --git a/apps/mobile/app/home.tsx b/apps/mobile/app/home.tsx index 338d82b..c142134 100644 --- a/apps/mobile/app/home.tsx +++ b/apps/mobile/app/home.tsx @@ -5,5 +5,10 @@ import { HomeScreen } from "../features/home/HomeScreen"; export default function HomeRoute() { const router = useRouter(); - return router.replace("/")} />; + return ( + router.push("/account")} + onLogout={() => router.replace("/")} + /> + ); } diff --git a/apps/mobile/features/account/AccountInfoScreen.tsx b/apps/mobile/features/account/AccountInfoScreen.tsx new file mode 100644 index 0000000..8e5919a --- /dev/null +++ b/apps/mobile/features/account/AccountInfoScreen.tsx @@ -0,0 +1,284 @@ +import { useState, type ComponentProps } from "react"; +import { + KeyboardAvoidingView, + Linking, + Platform, + Pressable, + SafeAreaView, + ScrollView, + Switch, + Text, + TextInput, + View, +} from "react-native"; + +import type { + AccountInfo, + AlertType, + DeliveryChannel, + NotificationPreferences, +} from "./types"; +import { accountStyles } from "./styles"; + +type AccountInfoScreenProps = { + account: AccountInfo; + onSave: (account: AccountInfo) => Promise; +}; + +const alertRows: Array<{ key: AlertType; label: string }> = [ + { key: "restock", label: "Restock alerts" }, + { key: "priceChange", label: "Price-change alerts" }, +]; + +function isValidEmail(value: string) { + return /^\S+@\S+\.\S+$/.test(value); +} + +function preferenceLabel(alertType: AlertType, channel: DeliveryChannel) { + const alertLabel = alertType === "restock" ? "Restock" : "Price-change"; + const channelLabel = channel === "push" ? "push" : "email"; + return `${alertLabel} ${channelLabel} alerts`; +} + +export function AccountInfoScreen({ account, onSave }: AccountInfoScreenProps) { + const [email, setEmail] = useState(account.email); + const [phone, setPhone] = useState(account.phone ?? ""); + const [preferences, setPreferences] = useState( + account.notificationPreferences, + ); + const [username, setUsername] = useState(account.username); + const [errors, setErrors] = useState([]); + const [isSaving, setIsSaving] = useState(false); + const [saveMessage, setSaveMessage] = useState(""); + + function updatePreference( + alertType: AlertType, + channel: DeliveryChannel, + value: boolean, + ) { + setPreferences((current) => ({ + ...current, + [alertType]: { ...current[alertType], [channel]: value }, + })); + } + + async function handleSave() { + const nextErrors: string[] = []; + const normalizedUsername = username.trim(); + const normalizedEmail = email.trim().toLowerCase(); + + if (!normalizedUsername) { + nextErrors.push("Enter a username."); + } + + if (!isValidEmail(normalizedEmail)) { + nextErrors.push("Enter a valid email address."); + } + + if (nextErrors.length > 0) { + setErrors(nextErrors); + setSaveMessage(""); + return; + } + + setErrors([]); + setIsSaving(true); + setSaveMessage(""); + + try { + await onSave({ + ...account, + email: normalizedEmail, + notificationPreferences: preferences, + pendingEmail: + normalizedEmail === account.verifiedEmail ? null : normalizedEmail, + phone: phone.trim() || null, + username: normalizedUsername, + }); + setSaveMessage("Changes saved for this session."); + } catch { + setSaveMessage("We could not save your changes. Try again."); + } finally { + setIsSaving(false); + } + } + + return ( + + + + + Account info + + + Update your details and choose how you receive alerts. + + + + Profile + + + + {account.pendingEmail ? ( + + Confirm {account.pendingEmail} to use it for email alerts. Until + then, alerts go to {account.verifiedEmail}. + + ) : null} + + + + Alert preferences + + + Monitoring continues even when every alert is off. + + {alertRows.map(({ key, label }) => ( + + {label} + + + + ))} + {account.pushAvailability === "unavailable" ? ( + + + Push notifications are unavailable on this device. + + + void Linking.openSettings().catch(() => { + setSaveMessage( + "Open your device settings to enable notifications.", + ); + }) + } + style={accountStyles.settingsLink} + > + + Open notification settings + + + + ) : null} + + {errors.map((error) => ( + + {error} + + ))} + {saveMessage ? ( + + {saveMessage} + + ) : null} + void handleSave()} + style={[ + accountStyles.button, + isSaving ? accountStyles.buttonDisabled : undefined, + ]} + > + + {isSaving ? "Saving changes…" : "Save changes"} + + + + + + ); +} + +type FieldProps = Pick< + ComponentProps, + "autoCapitalize" | "autoComplete" | "autoCorrect" | "keyboardType" +> & { + label: string; + onChangeText: (value: string) => void; + value: string; +}; + +function Field({ label, onChangeText, value, ...inputProps }: FieldProps) { + return ( + + {label} + + + ); +} + +type ToggleProps = { + alertType: AlertType; + channel: DeliveryChannel; + onValueChange: ( + alertType: AlertType, + channel: DeliveryChannel, + value: boolean, + ) => void; + preferences: NotificationPreferences; +}; + +function Toggle({ + alertType, + channel, + onValueChange, + preferences, +}: ToggleProps) { + const label = preferenceLabel(alertType, channel); + + return ( + + + {channel === "push" ? "Push" : "Email"} + + onValueChange(alertType, channel, value)} + value={preferences[alertType][channel]} + /> + + ); +} diff --git a/apps/mobile/features/account/styles.ts b/apps/mobile/features/account/styles.ts new file mode 100644 index 0000000..1862246 --- /dev/null +++ b/apps/mobile/features/account/styles.ts @@ -0,0 +1,60 @@ +import { StyleSheet } from "react-native"; + +export const accountStyles = StyleSheet.create({ + alertLabel: { color: "#FFFFFF", flex: 1, fontSize: 15, fontWeight: "600" }, + alertRow: { + alignItems: "center", + flexDirection: "row", + gap: 12, + marginBottom: 20, + }, + availabilityNotice: { marginBottom: 20 }, + button: { + alignItems: "center", + backgroundColor: "#FFFFFF", + borderRadius: 4, + justifyContent: "center", + marginTop: 8, + minHeight: 52, + }, + buttonDisabled: { opacity: 0.55 }, + buttonText: { color: "#000000", fontSize: 16, fontWeight: "700" }, + content: { flexGrow: 1, paddingHorizontal: 28, paddingVertical: 32 }, + helper: { color: "#BDBDBD", fontSize: 13, lineHeight: 18, marginBottom: 12 }, + input: { + borderBottomColor: "#7A7A7A", + borderBottomWidth: 1, + color: "#FFFFFF", + fontSize: 16, + minHeight: 48, + paddingHorizontal: 0, + }, + inputGroup: { marginBottom: 20 }, + label: { color: "#FFFFFF", fontSize: 14, fontWeight: "600", marginBottom: 4 }, + linkText: { color: "#FFFFFF", fontSize: 15, textDecorationLine: "underline" }, + screen: { backgroundColor: "#000000", flex: 1 }, + sectionTitle: { + color: "#FFFFFF", + fontSize: 20, + fontWeight: "700", + marginBottom: 16, + marginTop: 20, + }, + settingsLink: { alignSelf: "flex-start", paddingVertical: 4 }, + subtitle: { + color: "#BDBDBD", + fontSize: 16, + lineHeight: 22, + marginBottom: 16, + }, + title: { + color: "#FFFFFF", + fontSize: 32, + fontWeight: "700", + letterSpacing: -0.5, + marginBottom: 8, + }, + toggleGroup: { alignItems: "center", minWidth: 58 }, + toggleLabel: { color: "#BDBDBD", fontSize: 12, marginBottom: 4 }, + validation: { color: "#FFFFFF", fontSize: 14, marginBottom: 8 }, +}); diff --git a/apps/mobile/features/account/types.ts b/apps/mobile/features/account/types.ts new file mode 100644 index 0000000..a72c824 --- /dev/null +++ b/apps/mobile/features/account/types.ts @@ -0,0 +1,17 @@ +export type AlertType = "priceChange" | "restock"; +export type DeliveryChannel = "email" | "push"; + +export type NotificationPreferences = Record< + AlertType, + Record +>; + +export type AccountInfo = { + email: string; + notificationPreferences: NotificationPreferences; + pendingEmail: string | null; + phone: string | null; + pushAvailability: "available" | "unavailable"; + username: string; + verifiedEmail: string; +}; diff --git a/apps/mobile/features/auth/CreateAccountScreen.tsx b/apps/mobile/features/auth/CreateAccountScreen.tsx index 45fb682..41e7e32 100644 --- a/apps/mobile/features/auth/CreateAccountScreen.tsx +++ b/apps/mobile/features/auth/CreateAccountScreen.tsx @@ -28,8 +28,8 @@ export function CreateAccountScreen({ const [error, setError] = useState(""); function handleCreateAccount() { - if ((!email.trim() && !phone.trim()) || !username.trim() || !password) { - setError("Add an email or phone number, a username, and a password."); + if (!email.trim() || !username.trim() || !password) { + setError("Enter an email, a username, and a password."); return; } @@ -51,7 +51,7 @@ export function CreateAccountScreen({ Create your account. - Phone number is optional and will be used for text alerts later. + Add your account details to get started. @@ -76,7 +76,7 @@ export function CreateAccountScreen({ autoComplete="tel" keyboardType="phone-pad" onChangeText={setPhone} - placeholder="For future text alerts" + placeholder="Optional" placeholderTextColor="#7A7A7A" style={authStyles.input} value={phone} diff --git a/apps/mobile/features/home/HomeScreen.tsx b/apps/mobile/features/home/HomeScreen.tsx index c2fda01..8be9597 100644 --- a/apps/mobile/features/home/HomeScreen.tsx +++ b/apps/mobile/features/home/HomeScreen.tsx @@ -3,13 +3,23 @@ import { Pressable, SafeAreaView, Text, View } from "react-native"; import { authStyles } from "../auth/styles"; type HomeScreenProps = { + onAccountInfo?: () => void; onLogout: () => void; }; -export function HomeScreen({ onLogout }: HomeScreenProps) { +export function HomeScreen({ onAccountInfo, onLogout }: HomeScreenProps) { return ( + {onAccountInfo ? ( + + Account info + + ) : null} >; +} + +interface UpdateAccountInfoInput { + username: string; + email: string; + phone: string | null; + notificationPreferences: Record>; +} +``` + +- `GET /api/account` returns only the signed-in account's `AccountInfo`. +- `PATCH /api/account` atomically validates and updates the supplied non-email fields and preferences; when email changes, it records the replacement as pending verification while retaining the prior verified delivery address, then returns canonical account state. +- An unauthenticated request returns `401`; a request cannot name or modify another account. Invalid input returns the project's consistent `422` validation error shape. +- Email-address changes must use the identity provider's verified-email flow before becoming the active delivery address. The prior verified email remains active until verification completes; the exact managed-provider integration is deferred until the provider is selected. +- A push preference represents delivery intent, not proof that a current device can receive push. The response must expose non-sensitive push availability so the client can present a remediation state. + +## Data and Worker Invariant + +Store a per-account profile record plus four boolean preference values (or equivalently a normalized account-alert-delivery-preference table with uniqueness on account, alert kind, and delivery channel). All four preferences default to `false` at account creation. Preserve a verified email separately from a pending replacement email, and store device-registration/permission availability without overwriting the user's push preference. + +The worker pipeline must preserve this ordering: + +```text +Tracked item -> partner stock/price check -> event detection + persistence + -> preference lookup -> enabled delivery channels -> push/email +``` + +There must be no preference condition on the stock/price check or event-detection stages. A price decrease event is eligible once per changed observed price; the unchanged price must not produce repeated delivery. Integration tests must prove that an account with all four preferences disabled still produces the expected monitoring event and does not receive delivery attempts. + +## Security and Privacy Requirements + +- Authenticate every read/write and derive the account identity exclusively from the session/token; never accept an account ID from the client for these operations. +- Validate and normalize input at the API boundary: bounded username, valid normalized email, optional normalized phone, and exactly the supported preference keys/channels. +- Treat email and phone as PII: do not log them, do not return them in unrelated APIs, use TLS in transit, and apply the database encryption/access controls selected in the infrastructure plan. +- Require current authenticated-session protection and provider re-verification for an email change; return non-sensitive validation/conflict errors. +- Use parameterized database access, least-privilege worker/API service accounts, and audit security-sensitive profile/email changes without recording the raw values. +- A disabled notification preference is not consent to stop processing tracked-item monitoring data; explain this in nearby UI helper copy. + +## Commands and Verification Environment + +Current repository checks: + +```bash +npm run verify +npm run audit +npm run test:smoke +cd apps/mobile && npm test -- --runInBand +cd apps/mobile && npm run typecheck +``` + +When `apps/api` exists, add focused API and worker integration commands to its documented package scripts. Run them against disposable PostgreSQL/Testcontainers data only. + +## Implementation Plan + +### Task 1: Establish the contract, persistence migration, and safe defaults + +**Description:** Add the application-owned account/profile and notification-preference contract, migration, and account-creation defaults in the future API. Align onboarding so email is required and phone remains optional; remove the current misleading implication that phone enables future text alerts. + +**Acceptance criteria:** + +- [ ] Profile storage represents username, verified email, nullable phone, and every supported alert/channel preference with explicit defaults. +- [ ] All four preference defaults are `false`; profile storage preserves a pending email change and the prior verified delivery address. +- [ ] Registration cannot create an account without an email; it does not imply SMS delivery from a supplied phone number. +- [ ] The migration is reversible/tested according to the project's migration process and does not alter tracked-item monitoring records. + +**Verification:** + +- [ ] Migration and repository tests pass against disposable PostgreSQL. +- [ ] API contract/type tests cover the four preference values and nullable phone. + +**Dependencies:** Application-owned API and migration tooling must exist. + +**Likely files:** `apps/api` account module/migration/types, `apps/mobile/features/auth/CreateAccountScreen.tsx`, account-creation tests. + +### Task 2: Implement authenticated profile read/update behavior + +**Description:** Add `GET /api/account` and atomic `PATCH /api/account`, using the authenticated identity as the only account selector. + +**Acceptance criteria:** + +- [ ] A shopper can retrieve and update only their own canonical account information and all four preferences. +- [ ] Invalid profile fields or unsupported preference keys return a consistent validation error without partial persistence. +- [ ] Email change enters the selected identity provider's verification process while delivery remains on the prior verified email; sensitive values do not appear in logs or error payloads. +- [ ] Push preference remains enabled when device permission/token registration is unavailable, and the returned state lets the client explain how to restore availability. + +**Verification:** + +- [ ] API integration tests cover success, invalid input, unauthenticated access, cross-account access, and atomic update failure. +- [ ] Security review confirms boundary validation and no PII logging. + +**Dependencies:** Task 1; real authentication/session ownership in the API. + +**Likely files:** `apps/api` account controller/service/DTO/validation/tests. + +### Task 3: Preserve monitoring independently from delivery preferences + +**Description:** Implement the worker's event-to-delivery boundary so monitoring records restock/price-change events first, then applies enabled delivery channels. + +**Acceptance criteria:** + +- [ ] Partner polling and event detection run for every selected item regardless of any preference, verification, or device-permission state. +- [ ] Any observed price decrease produces one eligible event per changed price; enabled available channels receive it once according to delivery/idempotency rules, while disabled or unavailable channels receive no delivery attempt. +- [ ] Turning every toggle off still records the monitoring event and keeps the selected item eligible for later checks. + +**Verification:** + +- [ ] Worker integration tests exercise each alert type/channel combination and the all-disabled case. +- [ ] Test assertions distinguish event detection/persistence from push/email dispatch attempts. + +**Dependencies:** Tasks 1-2; tracked-item and stock-event worker foundations. + +**Likely files:** `apps/api` worker/event/delivery modules and integration tests. + +### Checkpoint: Backend behavior + +- [ ] Contract, authorization, migration, and worker invariant tests are green. +- [ ] A reviewer confirms that no preference gates monitoring or event persistence. + +### Task 4: Add the mobile Account Info screen and navigation + +**Description:** Add an authenticated route reachable from Home, with grouped profile fields and a clearly labeled Alert preferences section containing four accessible toggle controls. + +**Acceptance criteria:** + +- [ ] Username, email, and optional phone are editable; the empty phone field is saved as absent rather than a placeholder value. +- [ ] Restock and price-change rows each provide independent Push and Email controls, with helper copy stating that delivery settings do not stop item monitoring. +- [ ] The screen shows pending-email verification and push-unavailable states without changing the user's saved delivery choices; it has loading, error/retry, disabled-saving, validation, and save-success states and follows the existing black/white mobile styling without introducing a new UI library. + +**Verification:** + +- [ ] React Native Testing Library tests cover rendering, required-field validation, each toggle, retry, successful save, pending email verification, and push-unavailable remediation. +- [ ] Screen-reader labels/state and touch targets are manually checked on an Android emulator. + +**Dependencies:** Task 2 contract (or a deliberately isolated typed repository mock for an earlier UI-only slice). + +**Likely files:** `apps/mobile/app/account.tsx`, `apps/mobile/features/account/AccountInfoScreen.tsx`, account styles/client/types/tests, `apps/mobile/app/home.tsx`, `apps/mobile/features/home/HomeScreen.tsx`. + +### Task 5: Connect mobile state to the authenticated API and complete regression coverage + +**Description:** Replace any UI-only fixture with the account API client, preserve server-canonical values after save, and cover the full flow. + +**Acceptance criteria:** + +- [ ] The app loads and saves through the authenticated contract, displays safe failure states, and does not expose other-account data. +- [ ] Updated settings persist after a screen reload/session refresh. +- [ ] Existing login, account-creation, and logout prototype tests remain valid or are intentionally updated to reflect real authentication behavior. + +**Verification:** + +- [ ] Focused mobile tests plus API/worker integration suites pass. +- [ ] `npm run verify`, `npm run audit`, `npm run test:smoke`, `git diff --check`, and the mobile test/typecheck commands pass under Node 22.14.0. + +**Dependencies:** Tasks 2 and 4. + +**Likely files:** account API client/hook, mobile route/screen tests, API contract tests. + +### Checkpoint: Feature complete + +- [ ] Profile changes, all four delivery controls, validation, and persistence work end-to-end. +- [ ] The all-disabled test demonstrates continued event detection with zero delivery attempts. +- [ ] Accessibility and PII/authentication review are complete. + +## Dependency Order + +```text +Task 1 (contract + data) -> Task 2 (authorized API) -> Task 4 (mobile page) -> Task 5 (end-to-end) + \ + -> Task 3 (worker delivery boundary) +``` + +Tasks 3 and 4 may proceed in parallel after the contract and Task 2's ownership/validation behavior are stable. The migration and contract must be sequential and reviewed first. + +## Risks and Mitigations + +| Risk | Impact | Mitigation | +| -------------------------------------------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------- | +| Preferences accidentally gate polling | High | Keep event detection before preference lookup; make the all-disabled monitoring test a required regression test. | +| Email change enables delivery to an unverified address | High | Keep the prior verified email active until managed identity-provider verification completes. | +| Profile endpoint leaks or edits another user's data | High | Derive identity from authentication, enforce per-account authorization, and test cross-account attempts. | +| UI and worker interpret preference names differently | Medium | Define a shared finite contract for two alert kinds and two channels; contract-test it. | +| Existing prototype treats phone as an alternate login/contact and mentions texts | Medium | Make email required, retain phone as nullable profile data, and remove SMS language in the same change. | +| Push is enabled but cannot reach the active device | Medium | Preserve intent, surface availability/remediation in the client, and re-register tokens when permission returns. | +| Small price fluctuations create repeated alerts | Medium | Emit only on a decrease and deduplicate delivery for the same observed price. | + +## Deferred Decisions + +- The managed identity provider has not yet been selected, so the precise email-change re-verification flow is deferred. +- Push-token registration implementation, email provider selection, and notification deduplication retention windows belong to their respective future delivery specifications. From df22a286de0f78857bef6d5980f7f25d906df265 Mon Sep 17 00:00:00 2001 From: InsertUzername Date: Wed, 7 Oct 2026 18:15:56 -0700 Subject: [PATCH 03/12] feat: add account and notification prototype screens --- apps/mobile/__tests__/account-info.test.tsx | 20 ++-- apps/mobile/__tests__/auth-flow.test.tsx | 45 ++++++--- .../__tests__/notifications-screen.test.tsx | 37 +++++--- apps/mobile/app/notifications.tsx | 10 +- .../features/account/AccountInfoScreen.tsx | 2 +- .../features/auth/CreateAccountScreen.tsx | 2 +- apps/mobile/features/auth/LoginScreen.tsx | 2 +- apps/mobile/features/home/HomeScreen.tsx | 4 +- .../notifications/NotificationsScreen.tsx | 93 +++++-------------- apps/mobile/package-lock.json | 73 +++++---------- apps/mobile/package.json | 1 + 11 files changed, 134 insertions(+), 155 deletions(-) diff --git a/apps/mobile/__tests__/account-info.test.tsx b/apps/mobile/__tests__/account-info.test.tsx index e3e0912..29c862c 100644 --- a/apps/mobile/__tests__/account-info.test.tsx +++ b/apps/mobile/__tests__/account-info.test.tsx @@ -31,22 +31,24 @@ describe("AccountInfoScreen", () => { screen.getByText("Push notifications are unavailable on this device."), ).toBeTruthy(); - fireEvent.changeText(screen.getByLabelText("Username"), "new_name"); - fireEvent.changeText( + await fireEvent.changeText(screen.getByLabelText("Username"), "new_name"); + await fireEvent.changeText( screen.getByLabelText("Phone number (optional)"), "555-0100", ); - fireEvent( + await fireEvent( screen.getByLabelText("Restock push alerts"), "valueChange", true, ); - fireEvent( + await fireEvent( screen.getByLabelText("Price-change email alerts"), "valueChange", true, ); - fireEvent.press(screen.getByRole("button", { name: "Save changes" })); + await fireEvent.press( + screen.getByRole("button", { name: "Save changes" }), + ); await waitFor(() => { expect(onSave).toHaveBeenCalledWith({ @@ -68,9 +70,11 @@ describe("AccountInfoScreen", () => { , ); - fireEvent.changeText(screen.getByLabelText("Username"), " "); - fireEvent.changeText(screen.getByLabelText("Email"), "not-an-email"); - fireEvent.press(screen.getByRole("button", { name: "Save changes" })); + await fireEvent.changeText(screen.getByLabelText("Username"), " "); + await fireEvent.changeText(screen.getByLabelText("Email"), "not-an-email"); + await fireEvent.press( + screen.getByRole("button", { name: "Save changes" }), + ); expect(screen.getByText("Enter a username.")).toBeTruthy(); expect(screen.getByText("Enter a valid email address.")).toBeTruthy(); diff --git a/apps/mobile/__tests__/auth-flow.test.tsx b/apps/mobile/__tests__/auth-flow.test.tsx index 26e49ce..2fa6941 100644 --- a/apps/mobile/__tests__/auth-flow.test.tsx +++ b/apps/mobile/__tests__/auth-flow.test.tsx @@ -11,9 +11,12 @@ describe("local authentication prototype", () => { , ); - fireEvent.changeText(screen.getByLabelText("Username"), "quickcop_user"); - fireEvent.changeText(screen.getByLabelText("Password"), "password123"); - fireEvent.press(screen.getByRole("button", { name: "Log in" })); + await fireEvent.changeText( + screen.getByLabelText("Username"), + "quickcop_user", + ); + await fireEvent.changeText(screen.getByLabelText("Password"), "password123"); + await fireEvent.press(screen.getByRole("button", { name: "Log in" })); expect(onLogin).toHaveBeenCalledTimes(1); }); @@ -27,10 +30,18 @@ describe("local authentication prototype", () => { />, ); - fireEvent.changeText(screen.getByLabelText("Email"), "person@example.com"); - fireEvent.changeText(screen.getByLabelText("Username"), "quickcop_user"); - fireEvent.changeText(screen.getByLabelText("Password"), "password123"); - fireEvent.press(screen.getByRole("button", { name: "Create account" })); + await fireEvent.changeText( + screen.getByLabelText("Email"), + "person@example.com", + ); + await fireEvent.changeText( + screen.getByLabelText("Username"), + "quickcop_user", + ); + await fireEvent.changeText(screen.getByLabelText("Password"), "password123"); + await fireEvent.press( + screen.getByRole("button", { name: "Create account" }), + ); expect(onAccountCreated).toHaveBeenCalledTimes(1); }); @@ -44,13 +55,18 @@ describe("local authentication prototype", () => { />, ); - fireEvent.changeText( + await fireEvent.changeText( screen.getByLabelText("Phone number (optional)"), "555-0100", ); - fireEvent.changeText(screen.getByLabelText("Username"), "quickcop_user"); - fireEvent.changeText(screen.getByLabelText("Password"), "password123"); - fireEvent.press(screen.getByRole("button", { name: "Create account" })); + await fireEvent.changeText( + screen.getByLabelText("Username"), + "quickcop_user", + ); + await fireEvent.changeText(screen.getByLabelText("Password"), "password123"); + await fireEvent.press( + screen.getByRole("button", { name: "Create account" }), + ); expect(onAccountCreated).not.toHaveBeenCalled(); expect( @@ -60,9 +76,12 @@ describe("local authentication prototype", () => { it("returns to login when the user logs out from the empty home screen", async () => { const onLogout = jest.fn(); - const screen = await render(); + const screen = await render( + , + ); - fireEvent.press(screen.getByRole("button", { name: "Log out" })); + await fireEvent.press(screen.getByRole("button", { name: "Open menu" })); + await fireEvent.press(screen.getByRole("button", { name: "Log out" })); expect(onLogout).toHaveBeenCalledTimes(1); }); diff --git a/apps/mobile/__tests__/notifications-screen.test.tsx b/apps/mobile/__tests__/notifications-screen.test.tsx index 9493865..de8ab81 100644 --- a/apps/mobile/__tests__/notifications-screen.test.tsx +++ b/apps/mobile/__tests__/notifications-screen.test.tsx @@ -7,7 +7,9 @@ import { describe("NotificationsScreen", () => { it("shows example price-drop alerts and lets the user toggle an alert locally", async () => { - const screen = await render(); + const screen = await render( + , + ); expect(screen.getByText("Nintendo Switch 2")).toBeTruthy(); expect( @@ -29,7 +31,12 @@ describe("NotificationsScreen", () => { it("shows an empty state when no price alerts are available", async () => { const alerts: PriceAlert[] = []; - const screen = await render(); + const screen = await render( + , + ); expect(screen.getByText("No price alerts yet")).toBeTruthy(); expect( @@ -39,18 +46,24 @@ describe("NotificationsScreen", () => { ).toBeTruthy(); }); - it("lets the user toggle notification settings locally", async () => { - const screen = await render(); - const priceDropToggle = screen.getByRole("switch", { - name: "Price drop notifications", - }); + it("opens delivery preferences without rendering duplicate global settings", async () => { + const onManageDeliveryPreferences = jest.fn(); + const screen = await render( + , + ); - expect(priceDropToggle.props.value).toBe(true); - await fireEvent(priceDropToggle, "valueChange", false); + await fireEvent.press( + screen.getByRole("button", { name: "Manage delivery preferences" }), + ); + expect(onManageDeliveryPreferences).toHaveBeenCalledTimes(1); expect( - screen.getByRole("switch", { name: "Price drop notifications" }).props - .value, - ).toBe(false); + screen.queryByRole("switch", { name: "Price drop notifications" }), + ).toBeNull(); + expect( + screen.queryByRole("switch", { name: "Back in stock notifications" }), + ).toBeNull(); }); }); diff --git a/apps/mobile/app/notifications.tsx b/apps/mobile/app/notifications.tsx index 6ae35ba..4ab247b 100644 --- a/apps/mobile/app/notifications.tsx +++ b/apps/mobile/app/notifications.tsx @@ -1,5 +1,13 @@ +import { useRouter } from "expo-router"; + import { NotificationsScreen } from "../features/notifications/NotificationsScreen"; export default function NotificationsRoute() { - return ; + const router = useRouter(); + + return ( + router.push("/account")} + /> + ); } diff --git a/apps/mobile/features/account/AccountInfoScreen.tsx b/apps/mobile/features/account/AccountInfoScreen.tsx index 8e5919a..7aa263b 100644 --- a/apps/mobile/features/account/AccountInfoScreen.tsx +++ b/apps/mobile/features/account/AccountInfoScreen.tsx @@ -4,13 +4,13 @@ import { Linking, Platform, Pressable, - SafeAreaView, ScrollView, Switch, Text, TextInput, View, } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; import type { AccountInfo, diff --git a/apps/mobile/features/auth/CreateAccountScreen.tsx b/apps/mobile/features/auth/CreateAccountScreen.tsx index 41e7e32..bced414 100644 --- a/apps/mobile/features/auth/CreateAccountScreen.tsx +++ b/apps/mobile/features/auth/CreateAccountScreen.tsx @@ -3,12 +3,12 @@ import { KeyboardAvoidingView, Platform, Pressable, - SafeAreaView, ScrollView, Text, TextInput, View, } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; import { authStyles } from "./styles"; diff --git a/apps/mobile/features/auth/LoginScreen.tsx b/apps/mobile/features/auth/LoginScreen.tsx index 79c6ce9..9efd70a 100644 --- a/apps/mobile/features/auth/LoginScreen.tsx +++ b/apps/mobile/features/auth/LoginScreen.tsx @@ -3,11 +3,11 @@ import { KeyboardAvoidingView, Platform, Pressable, - SafeAreaView, ScrollView, Text, TextInput, } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; import { authStyles } from "./styles"; diff --git a/apps/mobile/features/home/HomeScreen.tsx b/apps/mobile/features/home/HomeScreen.tsx index dd7855d..11df237 100644 --- a/apps/mobile/features/home/HomeScreen.tsx +++ b/apps/mobile/features/home/HomeScreen.tsx @@ -2,12 +2,12 @@ import { useState } from "react"; import { Pressable, - SafeAreaView, ScrollView, StyleSheet, Text, View, } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; type HomeScreenProps = { onAccountInfo?: () => void; @@ -337,4 +337,4 @@ const styles = StyleSheet.create({ fontSize: 17, fontWeight: "700", }, -}); \ No newline at end of file +}); diff --git a/apps/mobile/features/notifications/NotificationsScreen.tsx b/apps/mobile/features/notifications/NotificationsScreen.tsx index 68fdc00..6395d2c 100644 --- a/apps/mobile/features/notifications/NotificationsScreen.tsx +++ b/apps/mobile/features/notifications/NotificationsScreen.tsx @@ -1,6 +1,13 @@ import { useState } from "react"; -import { ScrollView, StyleSheet, Switch, Text, View } from "react-native"; +import { + Pressable, + ScrollView, + StyleSheet, + Switch, + Text, + View, +} from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; export type PriceAlert = { @@ -36,16 +43,14 @@ const exampleAlerts: PriceAlert[] = [ type NotificationsScreenProps = { alerts?: PriceAlert[]; + onManageDeliveryPreferences: () => void; }; export function NotificationsScreen({ alerts = exampleAlerts, + onManageDeliveryPreferences, }: NotificationsScreenProps) { const [priceAlerts, setPriceAlerts] = useState(alerts); - const [priceDropNotificationsEnabled, setPriceDropNotificationsEnabled] = - useState(true); - const [backInStockNotificationsEnabled, setBackInStockNotificationsEnabled] = - useState(true); function setAlertEnabled(id: string, enabled: boolean) { setPriceAlerts((currentAlerts) => @@ -105,64 +110,24 @@ export function NotificationsScreen({ )} - - - Notification settings + + + Manage delivery preferences - - - - - - - These settings are shown for preview only and are not saved yet. + + Choose how you receive price-change and restock alerts. - + ); } -type SettingRowProps = { - description: string; - label: string; - onValueChange: (value: boolean) => void; - value: boolean; -}; - -function SettingRow({ - description, - label, - onValueChange, - value, -}: SettingRowProps) { - return ( - - - {label} - {description} - - - - ); -} - const styles = StyleSheet.create({ screen: { flex: 1, backgroundColor: "#000" }, content: { padding: 20, paddingBottom: 40 }, @@ -209,26 +174,18 @@ const styles = StyleSheet.create({ marginBottom: 6, }, emptyText: { color: "#aaa", fontSize: 15, lineHeight: 21 }, - settingsCard: { + preferencesLink: { backgroundColor: "#161616", borderColor: "#2b2b2b", borderRadius: 12, borderWidth: 1, - paddingHorizontal: 16, - }, - settingRow: { - alignItems: "center", - flexDirection: "row", - paddingVertical: 16, + padding: 16, }, - settingContent: { flex: 1, paddingRight: 12 }, - settingLabel: { + preferencesLinkText: { color: "#fff", fontSize: 16, fontWeight: "600", marginBottom: 4, }, - settingDescription: { color: "#aaa", fontSize: 14, lineHeight: 20 }, - divider: { backgroundColor: "#2b2b2b", height: 1 }, - helperText: { color: "#888", fontSize: 13, lineHeight: 19, marginTop: 10 }, + preferencesLinkDescription: { color: "#aaa", fontSize: 14, lineHeight: 20 }, }); diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json index 134defb..4b5e51b 100644 --- a/apps/mobile/package-lock.json +++ b/apps/mobile/package-lock.json @@ -39,6 +39,7 @@ "@types/react": "~19.2.2", "jest": "^29.7.0", "jest-expo": "57.0.4", + "test-renderer": "1.2.0", "typescript": "~6.0.3" }, "engines": { @@ -3238,7 +3239,6 @@ "integrity": "sha512-HZOXsKT0tGI9LlUw2LuedXsVeB88wFa536vVL0M6vE8zN63nI+sSr1ByxmPToP5K5bukaVscyeCJcF9guVNJ1g==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "*" } @@ -9790,12 +9790,6 @@ "react": "^19.2.3" } }, - "node_modules/react-dom/node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", @@ -10083,12 +10077,6 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, - "node_modules/react-native/node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, "node_modules/react-native/node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -10122,6 +10110,22 @@ } } }, + "node_modules/react-reconciler": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", + "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -10214,13 +10218,6 @@ "react": "^19.2.3" } }, - "node_modules/react-test-renderer/node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "dev": true, - "license": "MIT" - }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -10465,12 +10462,10 @@ } }, "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 + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", @@ -11138,37 +11133,19 @@ } }, "node_modules/test-renderer": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/test-renderer/-/test-renderer-1.3.0.tgz", - "integrity": "sha512-DXyHv7JrVmDc5dKpfVHFhJ0Evo/EFY3TqlNt8CR65ua3SCsI09K/mQDJmYUOnZz36GANYqjp0ApeRER/mOxktQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/test-renderer/-/test-renderer-1.2.0.tgz", + "integrity": "sha512-JYiEGbgBGtmHAWX8Kf99gGRL1HtjcRVHLrmJNTZL4vFs9XrnWcuL45Iszw/pO4094+JR7havSrN+ds6YTOpSQA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/react-reconciler": "~0.33.0", - "react-reconciler": "~0.34.0" + "react-reconciler": "~0.33.0" }, "peerDependencies": { "react": "^19.0.0" } }, - "node_modules/test-renderer/node_modules/react-reconciler": { - "version": "0.34.0", - "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.34.0.tgz", - "integrity": "sha512-eVibvB1fc9j5Qf1KRkiM3GzRPBdjg2P6G6QV2f3P6Eb3OGMxRBhdg2rrUwjbV5JrQaJj8nOa8uajmmJA5tJWjQ==", - "devOptional": true, - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.28.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "react": "^19.3.0" - } - }, "node_modules/throat": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", diff --git a/apps/mobile/package.json b/apps/mobile/package.json index ded33f0..c401a16 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -46,6 +46,7 @@ "@types/react": "~19.2.2", "jest": "^29.7.0", "jest-expo": "57.0.4", + "test-renderer": "1.2.0", "typescript": "~6.0.3" }, "jest": { From 68ccc6d72dd90b4bf4ca30d0a9f7735a41eb5e43 Mon Sep 17 00:00:00 2001 From: InsertUzername Date: Thu, 8 Oct 2026 18:42:07 -0700 Subject: [PATCH 04/12] wip and waiting for database --- apps/mobile/__tests__/account-info.test.tsx | 8 ++------ apps/mobile/__tests__/auth-flow.test.tsx | 15 ++++++++++++--- apps/mobile/app/home.tsx | 2 +- apps/mobile/features/home/HomeScreen.tsx | 8 +------- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/apps/mobile/__tests__/account-info.test.tsx b/apps/mobile/__tests__/account-info.test.tsx index 29c862c..a791db2 100644 --- a/apps/mobile/__tests__/account-info.test.tsx +++ b/apps/mobile/__tests__/account-info.test.tsx @@ -46,9 +46,7 @@ describe("AccountInfoScreen", () => { "valueChange", true, ); - await fireEvent.press( - screen.getByRole("button", { name: "Save changes" }), - ); + await fireEvent.press(screen.getByRole("button", { name: "Save changes" })); await waitFor(() => { expect(onSave).toHaveBeenCalledWith({ @@ -72,9 +70,7 @@ describe("AccountInfoScreen", () => { await fireEvent.changeText(screen.getByLabelText("Username"), " "); await fireEvent.changeText(screen.getByLabelText("Email"), "not-an-email"); - await fireEvent.press( - screen.getByRole("button", { name: "Save changes" }), - ); + await fireEvent.press(screen.getByRole("button", { name: "Save changes" })); expect(screen.getByText("Enter a username.")).toBeTruthy(); expect(screen.getByText("Enter a valid email address.")).toBeTruthy(); diff --git a/apps/mobile/__tests__/auth-flow.test.tsx b/apps/mobile/__tests__/auth-flow.test.tsx index 2fa6941..fe4d2e1 100644 --- a/apps/mobile/__tests__/auth-flow.test.tsx +++ b/apps/mobile/__tests__/auth-flow.test.tsx @@ -15,7 +15,10 @@ describe("local authentication prototype", () => { screen.getByLabelText("Username"), "quickcop_user", ); - await fireEvent.changeText(screen.getByLabelText("Password"), "password123"); + await fireEvent.changeText( + screen.getByLabelText("Password"), + "password123", + ); await fireEvent.press(screen.getByRole("button", { name: "Log in" })); expect(onLogin).toHaveBeenCalledTimes(1); @@ -38,7 +41,10 @@ describe("local authentication prototype", () => { screen.getByLabelText("Username"), "quickcop_user", ); - await fireEvent.changeText(screen.getByLabelText("Password"), "password123"); + await fireEvent.changeText( + screen.getByLabelText("Password"), + "password123", + ); await fireEvent.press( screen.getByRole("button", { name: "Create account" }), ); @@ -63,7 +69,10 @@ describe("local authentication prototype", () => { screen.getByLabelText("Username"), "quickcop_user", ); - await fireEvent.changeText(screen.getByLabelText("Password"), "password123"); + await fireEvent.changeText( + screen.getByLabelText("Password"), + "password123", + ); await fireEvent.press( screen.getByRole("button", { name: "Create account" }), ); diff --git a/apps/mobile/app/home.tsx b/apps/mobile/app/home.tsx index 9ed32e1..0bace89 100644 --- a/apps/mobile/app/home.tsx +++ b/apps/mobile/app/home.tsx @@ -12,4 +12,4 @@ export default function HomeRoute() { onOpenNotifications={() => router.push("/notifications")} /> ); -} \ No newline at end of file +} diff --git a/apps/mobile/features/home/HomeScreen.tsx b/apps/mobile/features/home/HomeScreen.tsx index 11df237..db54e7a 100644 --- a/apps/mobile/features/home/HomeScreen.tsx +++ b/apps/mobile/features/home/HomeScreen.tsx @@ -1,12 +1,6 @@ import { useState } from "react"; -import { - Pressable, - ScrollView, - StyleSheet, - Text, - View, -} from "react-native"; +import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; type HomeScreenProps = { From bdaa1bf7b7ea9bff7d308eb193449c3d612df757 Mon Sep 17 00:00:00 2001 From: InsertUzername Date: Mon, 19 Oct 2026 00:23:38 -0700 Subject: [PATCH 05/12] Implement Supabase mobile authentication boundary --- apps/mobile/.env.example | 2 + apps/mobile/README.md | 13 +- apps/mobile/__tests__/auth-flow.test.tsx | 149 ++++++++--------- apps/mobile/__tests__/auth-service.test.ts | 55 +++++++ apps/mobile/__tests__/supabase-config.test.ts | 46 ++++++ apps/mobile/app/{ => (protected)}/account.tsx | 4 +- apps/mobile/app/(protected)/home.tsx | 23 +++ .../app/{ => (protected)}/notifications.tsx | 4 +- apps/mobile/app/(public)/create-account.tsx | 14 ++ apps/mobile/app/(public)/index.tsx | 20 +++ apps/mobile/app/_layout.tsx | 34 +++- apps/mobile/app/create-account.tsx | 14 -- apps/mobile/app/home.tsx | 15 -- apps/mobile/app/index.tsx | 14 -- apps/mobile/features/auth/AuthProvider.tsx | 104 ++++++++++-- .../auth/ConfigurationErrorScreen.tsx | 29 ++++ .../features/auth/CreateAccountScreen.tsx | 67 +++----- apps/mobile/features/auth/LoginScreen.tsx | 86 +++++----- apps/mobile/features/auth/authService.ts | 151 ++++++++++++++++++ apps/mobile/features/home/HomeScreen.tsx | 7 +- apps/mobile/lib/secureStorage.ts | 7 + apps/mobile/lib/supabase.ts | 45 ++++-- apps/mobile/lib/supabaseConfig.ts | 93 +++++++++++ apps/mobile/package-lock.json | 59 ++----- apps/mobile/package.json | 6 +- infrastructure_plan.md | 114 ++++++------- 26 files changed, 814 insertions(+), 361 deletions(-) create mode 100644 apps/mobile/.env.example create mode 100644 apps/mobile/__tests__/auth-service.test.ts create mode 100644 apps/mobile/__tests__/supabase-config.test.ts rename apps/mobile/app/{ => (protected)}/account.tsx (80%) create mode 100644 apps/mobile/app/(protected)/home.tsx rename apps/mobile/app/{ => (protected)}/notifications.tsx (50%) create mode 100644 apps/mobile/app/(public)/create-account.tsx create mode 100644 apps/mobile/app/(public)/index.tsx delete mode 100644 apps/mobile/app/create-account.tsx delete mode 100644 apps/mobile/app/home.tsx delete mode 100644 apps/mobile/app/index.tsx create mode 100644 apps/mobile/features/auth/ConfigurationErrorScreen.tsx create mode 100644 apps/mobile/features/auth/authService.ts create mode 100644 apps/mobile/lib/secureStorage.ts create mode 100644 apps/mobile/lib/supabaseConfig.ts diff --git a/apps/mobile/.env.example b/apps/mobile/.env.example new file mode 100644 index 0000000..8c59d69 --- /dev/null +++ b/apps/mobile/.env.example @@ -0,0 +1,2 @@ +EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co +EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_replace-me diff --git a/apps/mobile/README.md b/apps/mobile/README.md index fb8c1c2..21c2c8d 100644 --- a/apps/mobile/README.md +++ b/apps/mobile/README.md @@ -1,6 +1,6 @@ # QuickCop mobile -Expo/React Native Android foundation. Authentication is intentionally local-only for this first UI slice: entered email, phone number, username, and password are never persisted, logged, or sent over the network. +Expo/React Native Android client. Authentication uses Supabase Auth with email/password credentials. Only the public project URL and publishable key belong in the mobile configuration; never put service-role keys, database passwords, or access tokens in Expo variables. ## Commands @@ -14,7 +14,16 @@ npm test -- --runInBand npm run typecheck ``` -The login screen is the initial route. A valid local login or account-creation form takes the user to an empty home screen; **Log out** returns to login. +Set these values in a local, uncommitted `apps/mobile/.env` file: + +```dotenv +EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co +EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_... +``` + +For local Supabase development, use the local project URL appropriate to the emulator (`http://10.0.2.2:54321` for Android). EAS builds should receive the same two public values through EAS environment variables. The app fails closed when either value is missing or invalid. + +The login screen is the initial public route. Protected routes require a restored Supabase session; **Log out** clears the device session immediately and reports if remote revocation could not be completed. ## Android SDK launch diff --git a/apps/mobile/__tests__/auth-flow.test.tsx b/apps/mobile/__tests__/auth-flow.test.tsx index 1a0c54a..9a1aeaa 100644 --- a/apps/mobile/__tests__/auth-flow.test.tsx +++ b/apps/mobile/__tests__/auth-flow.test.tsx @@ -1,106 +1,103 @@ -import { fireEvent, render, waitFor } from "@testing-library/react-native"; +import { + fireEvent, + render, + userEvent, + waitFor, +} from "@testing-library/react-native"; import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { CreateAccountScreen } from "../features/auth/CreateAccountScreen"; import { LoginScreen } from "../features/auth/LoginScreen"; import { HomeScreen } from "../features/home/HomeScreen"; -import { supabase } from "../lib/supabase"; - -jest.mock("../lib/supabase", () => ({ - supabase: { - rpc: jest.fn(), - auth: { - signUp: jest.fn(), - signInWithPassword: jest.fn(), - signOut: jest.fn(), - }, - }, -})); +import * as authService from "../features/auth/authService"; -// Loosely typed so the mocks accept any resolved value -type MockFn = jest.Mock<(...args: unknown[]) => Promise>; +jest.mock("../features/auth/authService", () => ({ + signInWithEmail: jest.fn(), + signUpWithEmail: jest.fn(), + signOut: jest.fn(), +})); -const mockSupabase = supabase as unknown as { - rpc: MockFn; - auth: { - signUp: MockFn; - signInWithPassword: MockFn; - signOut: MockFn; - }; -}; +const mockAuthService = authService as jest.Mocked; beforeEach(() => { jest.clearAllMocks(); - // username_available -> true, get_email_for_username -> an email - mockSupabase.rpc.mockImplementation((fn: unknown) => - Promise.resolve({ - data: fn === "username_available" ? true : "person@example.com", - error: null, - }), - ); - mockSupabase.auth.signUp.mockResolvedValue({ - data: { session: {} }, - error: null, + mockAuthService.signUpWithEmail.mockResolvedValue({ + ok: true, + sessionCreated: true, }); - mockSupabase.auth.signInWithPassword.mockResolvedValue({ error: null }); - mockSupabase.auth.signOut.mockResolvedValue({ error: null }); + mockAuthService.signInWithEmail.mockResolvedValue({ ok: true }); + mockAuthService.signOut.mockResolvedValue({ ok: true }); }); describe("login", () => { async function submitLogin(onLogin = jest.fn()) { + const user = userEvent.setup(); const screen = await render( , ); - await fireEvent.changeText( - screen.getByLabelText("Username"), - "quickcop_user", - ); - await fireEvent.changeText( - screen.getByLabelText("Password"), - "password123", - ); - await fireEvent.press(screen.getByRole("button", { name: "Log in" })); + await user.type(screen.getByLabelText("Email"), "person@example.com"); + await user.type(screen.getByLabelText("Password"), "password123"); + await user.press(screen.getByRole("button", { name: "Log in" })); return { screen, onLogin }; } - it("looks up the email, signs in, and opens the home screen", async () => { + it("signs in with email and password", async () => { + mockAuthService.signInWithEmail.mockResolvedValue({ ok: true }); const { onLogin } = await submitLogin(); await waitFor(() => expect(onLogin).toHaveBeenCalledTimes(1)); - expect(mockSupabase.rpc).toHaveBeenCalledWith("get_email_for_username", { - p_username: "quickcop_user", - }); - expect(mockSupabase.auth.signInWithPassword).toHaveBeenCalledWith({ - email: "person@example.com", - password: "password123", - }); + expect(mockAuthService.signInWithEmail).toHaveBeenCalledWith( + "person@example.com", + "password123", + ); }); it("shows an error when the password is wrong", async () => { - mockSupabase.auth.signInWithPassword.mockResolvedValue({ - error: { message: "Invalid login credentials" }, + mockAuthService.signInWithEmail.mockResolvedValue({ + ok: false, + failure: { kind: "credentials" }, + }); + + const { screen, onLogin } = await submitLogin(); + + await waitFor(() => + expect(screen.getByText("Invalid email or password.")).toBeTruthy(), + ); + expect(onLogin).not.toHaveBeenCalled(); + }); + + it("shows a retryable message when the network request fails", async () => { + mockAuthService.signInWithEmail.mockResolvedValue({ + ok: false, + failure: { kind: "network" }, }); const { screen, onLogin } = await submitLogin(); await waitFor(() => - expect(screen.getByText("Invalid username or password.")).toBeTruthy(), + expect(screen.getByText("Unable to connect. Try again.")).toBeTruthy(), ); expect(onLogin).not.toHaveBeenCalled(); }); - it("shows the same error when the username does not exist", async () => { - mockSupabase.rpc.mockResolvedValue({ data: null, error: null }); + it("does not expose unexpected provider errors as credential failures", async () => { + mockAuthService.signInWithEmail.mockResolvedValue({ + ok: false, + failure: { kind: "unknown" }, + }); const { screen, onLogin } = await submitLogin(); await waitFor(() => - expect(screen.getByText("Invalid username or password.")).toBeTruthy(), + expect( + screen.getByText( + "Authentication is temporarily unavailable. Try again later.", + ), + ).toBeTruthy(), ); - expect(mockSupabase.auth.signInWithPassword).not.toHaveBeenCalled(); expect(onLogin).not.toHaveBeenCalled(); }); }); @@ -137,35 +134,26 @@ describe("create account", () => { const { onAccountCreated } = await submitSignUp(); await waitFor(() => expect(onAccountCreated).toHaveBeenCalledTimes(1)); - expect(mockSupabase.auth.signUp).toHaveBeenCalledWith({ + expect(mockAuthService.signUpWithEmail).toHaveBeenCalledWith({ email: "person@example.com", password: "password123", - options: { - data: { - username: "quickcop_user", - display_name: "quickcop_user", - phone: null, - }, - }, + username: "quickcop_user", + phone: null, }); }); - it("rejects a taken username without creating an account", async () => { - mockSupabase.rpc.mockResolvedValue({ data: false, error: null }); - + it("submits account creation without a client-side username lookup", async () => { const { screen, onAccountCreated } = await submitSignUp(); - await waitFor(() => - expect(screen.getByText("That username is taken.")).toBeTruthy(), - ); - expect(mockSupabase.auth.signUp).not.toHaveBeenCalled(); - expect(onAccountCreated).not.toHaveBeenCalled(); + await waitFor(() => expect(onAccountCreated).toHaveBeenCalledTimes(1)); + expect(mockAuthService.signUpWithEmail).toHaveBeenCalledTimes(1); + expect(screen.queryByText("That username is taken.")).toBeNull(); }); it("asks the user to confirm their email when no session is returned", async () => { - mockSupabase.auth.signUp.mockResolvedValue({ - data: { session: null }, - error: null, + mockAuthService.signUpWithEmail.mockResolvedValue({ + ok: true, + sessionCreated: false, }); const { screen, onAccountCreated } = await submitSignUp(); @@ -182,8 +170,8 @@ describe("create account", () => { }); describe("logout", () => { - it("signs out and returns to login from the home menu", async () => { - const onLogout = jest.fn(); + it("calls the route logout handler from the home menu", async () => { + const onLogout = jest.fn(() => undefined); const screen = await render( , ); @@ -193,6 +181,5 @@ describe("logout", () => { await fireEvent.press(screen.getByRole("button", { name: "Log out" })); await waitFor(() => expect(onLogout).toHaveBeenCalledTimes(1)); - expect(mockSupabase.auth.signOut).toHaveBeenCalledTimes(1); }); }); diff --git a/apps/mobile/__tests__/auth-service.test.ts b/apps/mobile/__tests__/auth-service.test.ts new file mode 100644 index 0000000..3be5769 --- /dev/null +++ b/apps/mobile/__tests__/auth-service.test.ts @@ -0,0 +1,55 @@ +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; + +import { signInWithEmail, signOut } from "../features/auth/authService"; +import { supabase } from "../lib/supabase"; + +jest.mock("../lib/supabase", () => ({ + supabase: { + auth: { + signInWithPassword: jest.fn(), + signOut: jest.fn(), + }, + }, +})); + +const mockAuth = (supabase as NonNullable).auth as unknown as { + signInWithPassword: jest.MockedFunction< + (input: { + email: string; + password: string; + }) => Promise<{ error: unknown | null }> + >; + signOut: jest.MockedFunction< + (options?: { + scope: "global" | "local"; + }) => Promise<{ error: unknown | null }> + >; +}; + +beforeEach(() => { + jest.clearAllMocks(); + mockAuth.signInWithPassword.mockResolvedValue({ error: null }); + mockAuth.signOut.mockResolvedValue({ error: null }); +}); + +describe("authService", () => { + it("classifies unexpected sign-in failures separately from credentials", async () => { + mockAuth.signInWithPassword.mockRejectedValue( + new Error("Unexpected provider failure"), + ); + + await expect( + signInWithEmail("person@example.com", "password123"), + ).resolves.toEqual({ + ok: false, + failure: { kind: "unknown" }, + }); + }); + + it("clears local auth while attempting remote revocation", async () => { + await expect(signOut()).resolves.toEqual({ ok: true }); + + expect(mockAuth.signOut).toHaveBeenNthCalledWith(1, { scope: "global" }); + expect(mockAuth.signOut).toHaveBeenNthCalledWith(2, { scope: "local" }); + }); +}); diff --git a/apps/mobile/__tests__/supabase-config.test.ts b/apps/mobile/__tests__/supabase-config.test.ts new file mode 100644 index 0000000..0e4cbd6 --- /dev/null +++ b/apps/mobile/__tests__/supabase-config.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "@jest/globals"; + +import { + readSupabaseConfig, + SupabaseConfigurationError, +} from "../lib/supabaseConfig"; + +describe("readSupabaseConfig", () => { + it("requires the public project URL and publishable key", () => { + expect(() => readSupabaseConfig({})).toThrow( + "Missing EXPO_PUBLIC_SUPABASE_URL or EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY.", + ); + }); + + it("allows HTTPS project URLs", () => { + expect( + readSupabaseConfig({ + EXPO_PUBLIC_SUPABASE_URL: "https://quickcop.supabase.co", + EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY: "sb_publishable_test", + }), + ).toEqual({ + url: "https://quickcop.supabase.co", + publishableKey: "sb_publishable_test", + }); + }); + + it("allows HTTP only for local development hosts", () => { + expect( + readSupabaseConfig({ + EXPO_PUBLIC_SUPABASE_URL: "http://10.0.2.2:54321", + EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY: "sb_publishable_test", + }).url, + ).toBe("http://10.0.2.2:54321"); + }); + + it("rejects service-role JWTs in the public key slot", () => { + const payload = "eyJyb2xlIjoic2VydmljZV9yb2xlIn0="; + + expect(() => + readSupabaseConfig({ + EXPO_PUBLIC_SUPABASE_URL: "https://quickcop.supabase.co", + EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY: `header.${payload}.signature`, + }), + ).toThrow(SupabaseConfigurationError); + }); +}); diff --git a/apps/mobile/app/account.tsx b/apps/mobile/app/(protected)/account.tsx similarity index 80% rename from apps/mobile/app/account.tsx rename to apps/mobile/app/(protected)/account.tsx index 095ebcf..83a6110 100644 --- a/apps/mobile/app/account.tsx +++ b/apps/mobile/app/(protected)/account.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; -import { AccountInfoScreen } from "../features/account/AccountInfoScreen"; -import type { AccountInfo } from "../features/account/types"; +import { AccountInfoScreen } from "../../features/account/AccountInfoScreen"; +import type { AccountInfo } from "../../features/account/types"; const prototypeAccount: AccountInfo = { email: "shopper@example.com", diff --git a/apps/mobile/app/(protected)/home.tsx b/apps/mobile/app/(protected)/home.tsx new file mode 100644 index 0000000..5084c1c --- /dev/null +++ b/apps/mobile/app/(protected)/home.tsx @@ -0,0 +1,23 @@ +import { useRouter } from "expo-router"; + +import { useAuth } from "../../features/auth/AuthProvider"; +import { HomeScreen } from "../../features/home/HomeScreen"; + +export default function HomeRoute() { + const router = useRouter(); + const { logout } = useAuth(); + + return ( + router.push("/(protected)/account")} + onLogout={async () => { + const result = await logout(); + router.replace({ + pathname: "/(public)", + params: result.ok ? undefined : { logout: "failed" }, + }); + }} + onOpenNotifications={() => router.push("/(protected)/notifications")} + /> + ); +} diff --git a/apps/mobile/app/notifications.tsx b/apps/mobile/app/(protected)/notifications.tsx similarity index 50% rename from apps/mobile/app/notifications.tsx rename to apps/mobile/app/(protected)/notifications.tsx index 4ab247b..4d36831 100644 --- a/apps/mobile/app/notifications.tsx +++ b/apps/mobile/app/(protected)/notifications.tsx @@ -1,13 +1,13 @@ import { useRouter } from "expo-router"; -import { NotificationsScreen } from "../features/notifications/NotificationsScreen"; +import { NotificationsScreen } from "../../features/notifications/NotificationsScreen"; export default function NotificationsRoute() { const router = useRouter(); return ( router.push("/account")} + onManageDeliveryPreferences={() => router.push("/(protected)/account")} /> ); } diff --git a/apps/mobile/app/(public)/create-account.tsx b/apps/mobile/app/(public)/create-account.tsx new file mode 100644 index 0000000..1be4825 --- /dev/null +++ b/apps/mobile/app/(public)/create-account.tsx @@ -0,0 +1,14 @@ +import { useRouter } from "expo-router"; + +import { CreateAccountScreen } from "../../features/auth/CreateAccountScreen"; + +export default function CreateAccountRoute() { + const router = useRouter(); + + return ( + router.replace("/(protected)/home")} + onBackToLogin={() => router.replace("/(public)")} + /> + ); +} diff --git a/apps/mobile/app/(public)/index.tsx b/apps/mobile/app/(public)/index.tsx new file mode 100644 index 0000000..1a487ca --- /dev/null +++ b/apps/mobile/app/(public)/index.tsx @@ -0,0 +1,20 @@ +import { useLocalSearchParams, useRouter } from "expo-router"; + +import { LoginScreen } from "../../features/auth/LoginScreen"; + +export default function LoginRoute() { + const router = useRouter(); + const { logout } = useLocalSearchParams<{ logout?: string }>(); + + return ( + router.push("/(public)/create-account")} + onLogin={() => router.replace("/(protected)/home")} + /> + ); +} diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index 0ed90e5..5999d66 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -2,29 +2,47 @@ import { Stack, useRouter, useSegments } from "expo-router"; import { StatusBar } from "expo-status-bar"; import { useEffect } from "react"; +import { ConfigurationErrorScreen } from "../features/auth/ConfigurationErrorScreen"; import { AuthProvider, useAuth } from "../features/auth/AuthProvider"; +import { supabaseConfigError } from "../lib/supabase"; function RootNavigator() { - const { session, loading } = useAuth(); + const { authState, retrySessionRestore } = useAuth(); const segments = useSegments(); const router = useRouter(); useEffect(() => { - if (loading) return; + if (authState.status === "loading" || authState.status === "error") return; - const onHome = segments[0] === "home"; + const inProtectedRoutes = segments[0] === "(protected)"; + const inPublicRoutes = segments[0] === "(public)"; - if (session && !onHome) { - router.replace("/home"); - } else if (!session && onHome) { - router.replace("/"); + if (authState.status === "authenticated" && !inProtectedRoutes) { + router.replace("/(protected)/home"); + } else if (authState.status === "unauthenticated" && !inPublicRoutes) { + router.replace("/(public)"); } - }, [session, loading, segments]); + }, [authState, router, segments]); + + if (authState.status === "loading") return null; + + if (authState.status === "error") { + return ( + + ); + } return ; } export default function RootLayout() { + if (supabaseConfigError) { + return ; + } + return ( diff --git a/apps/mobile/app/create-account.tsx b/apps/mobile/app/create-account.tsx deleted file mode 100644 index 4093f2a..0000000 --- a/apps/mobile/app/create-account.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { useRouter } from "expo-router"; - -import { CreateAccountScreen } from "../features/auth/CreateAccountScreen"; - -export default function CreateAccountRoute() { - const router = useRouter(); - - return ( - router.replace("/home")} - onBackToLogin={() => router.back()} - /> - ); -} diff --git a/apps/mobile/app/home.tsx b/apps/mobile/app/home.tsx deleted file mode 100644 index 0bace89..0000000 --- a/apps/mobile/app/home.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useRouter } from "expo-router"; - -import { HomeScreen } from "../features/home/HomeScreen"; - -export default function HomeRoute() { - const router = useRouter(); - - return ( - router.push("/account")} - onLogout={() => router.replace("/")} - onOpenNotifications={() => router.push("/notifications")} - /> - ); -} diff --git a/apps/mobile/app/index.tsx b/apps/mobile/app/index.tsx deleted file mode 100644 index 8454765..0000000 --- a/apps/mobile/app/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { useRouter } from "expo-router"; - -import { LoginScreen } from "../features/auth/LoginScreen"; - -export default function LoginRoute() { - const router = useRouter(); - - return ( - router.push("/create-account")} - onLogin={() => router.replace("/home")} - /> - ); -} diff --git a/apps/mobile/features/auth/AuthProvider.tsx b/apps/mobile/features/auth/AuthProvider.tsx index 276a77c..af8d01e 100644 --- a/apps/mobile/features/auth/AuthProvider.tsx +++ b/apps/mobile/features/auth/AuthProvider.tsx @@ -1,39 +1,113 @@ import { Session } from "@supabase/supabase-js"; -import { createContext, useContext, useEffect, useState } from "react"; +import { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from "react"; import { supabase } from "../../lib/supabase"; +import { AUTH_REQUEST_TIMEOUT_MS, signOut, withTimeout } from "./authService"; + +export type AuthState = + | { status: "loading" } + | { status: "authenticated"; session: Session } + | { status: "unauthenticated" } + | { status: "error"; message: string }; type AuthContextType = { - session: Session | null; - loading: boolean; + authState: AuthState; + logout: () => Promise<{ ok: boolean }>; + retrySessionRestore: () => void; }; const AuthContext = createContext({ - session: null, - loading: true, + authState: { status: "loading" }, + logout: async () => ({ ok: false }), + retrySessionRestore: () => undefined, }); export function AuthProvider({ children }: { children: React.ReactNode }) { - const [session, setSession] = useState(null); - const [loading, setLoading] = useState(true); + const [authState, setAuthState] = useState({ status: "loading" }); + const [retryCount, setRetryCount] = useState(0); + + const retrySessionRestore = useCallback(() => { + setRetryCount((current) => current + 1); + }, []); + + const logout = useCallback(async () => { + setAuthState({ status: "unauthenticated" }); + return signOut(); + }, []); useEffect(() => { - supabase.auth.getSession().then(({ data }) => { - setSession(data.session); - setLoading(false); - }); + let mounted = true; + + setAuthState({ status: "loading" }); + + if (!supabase) { + setAuthState({ + status: "error", + message: "Authentication is unavailable.", + }); + return () => { + mounted = false; + }; + } + + let authEventReceived = false; const { data: listener } = supabase.auth.onAuthStateChange( (_event, newSession) => { - setSession(newSession); + if (!mounted) return; + authEventReceived = true; + setAuthState( + newSession + ? { status: "authenticated", session: newSession } + : { status: "unauthenticated" }, + ); }, ); - return () => listener.subscription.unsubscribe(); - }, []); + void withTimeout( + supabase.auth.getSession(), + AUTH_REQUEST_TIMEOUT_MS, + "Session restoration timed out.", + ) + .then(({ data, error }) => { + if (!mounted || authEventReceived) return; + if (error) { + setAuthState({ + status: "error", + message: "Unable to restore your session. Try again.", + }); + return; + } + + setAuthState( + data.session + ? { status: "authenticated", session: data.session } + : { status: "unauthenticated" }, + ); + }) + .catch(() => { + if (mounted && !authEventReceived) { + setAuthState({ + status: "error", + message: "Unable to restore your session. Try again.", + }); + } + }); + + return () => { + mounted = false; + listener.subscription.unsubscribe(); + }; + }, [retryCount]); return ( - + {children} ); diff --git a/apps/mobile/features/auth/ConfigurationErrorScreen.tsx b/apps/mobile/features/auth/ConfigurationErrorScreen.tsx new file mode 100644 index 0000000..ef0d20e --- /dev/null +++ b/apps/mobile/features/auth/ConfigurationErrorScreen.tsx @@ -0,0 +1,29 @@ +import { Pressable, SafeAreaView, Text } from "react-native"; + +type ConfigurationErrorScreenProps = { + message: string; + onRetry?: () => void; +}; + +export function ConfigurationErrorScreen({ + message, + onRetry, +}: ConfigurationErrorScreenProps) { + return ( + + + Authentication is unavailable + + {message} + {onRetry ? ( + + Retry + + ) : null} + + ); +} diff --git a/apps/mobile/features/auth/CreateAccountScreen.tsx b/apps/mobile/features/auth/CreateAccountScreen.tsx index f1282ff..e108010 100644 --- a/apps/mobile/features/auth/CreateAccountScreen.tsx +++ b/apps/mobile/features/auth/CreateAccountScreen.tsx @@ -10,8 +10,8 @@ import { } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import { supabase } from "../../lib/supabase"; import { authStyles } from "./styles"; +import { signUpWithEmail } from "./authService"; type CreateAccountScreenProps = { onAccountCreated: () => void; @@ -60,49 +60,32 @@ export function CreateAccountScreen({ setError(""); setLoading(true); - // 1. Check the username is free - const { data: available, error: rpcError } = await supabase.rpc( - "username_available", - { p_username: trimmedUsername }, - ); - if (rpcError) { - console.log("username_available error:", rpcError); + try { + const result = await signUpWithEmail({ + email: trimmedEmail, + password, + phone: trimmedPhone || null, + username: trimmedUsername, + }); + + if (!result.ok) { + setError( + result.failure.kind === "network" + ? "Unable to connect. Try again." + : "We could not create your account. Check your details and try again.", + ); + return; + } + + if (!result.sessionCreated) { + setNotice("Check your email to confirm your account, then log in."); + return; + } + + onAccountCreated(); + } finally { setLoading(false); - setError(rpcError.message); - return; - } - if (!available) { - setLoading(false); - setError("That username is taken."); - return; } - - // 2. Create the account (the database trigger creates the profile row) - const { data, error: signUpError } = await supabase.auth.signUp({ - email: trimmedEmail, - password, - options: { - data: { - username: trimmedUsername, - display_name: trimmedUsername, - phone: trimmedPhone || null, - }, - }, - }); - setLoading(false); - - if (signUpError) { - setError(signUpError.message); - return; - } - - // If email confirmation is ON in Supabase, there's no session yet - if (!data.session) { - setNotice("Check your email to confirm your account, then log in."); - return; - } - - onAccountCreated(); } return ( diff --git a/apps/mobile/features/auth/LoginScreen.tsx b/apps/mobile/features/auth/LoginScreen.tsx index e85c5b6..0411faa 100644 --- a/apps/mobile/features/auth/LoginScreen.tsx +++ b/apps/mobile/features/auth/LoginScreen.tsx @@ -9,66 +9,60 @@ import { } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import { supabase } from "../../lib/supabase"; import { authStyles } from "./styles"; +import { signInWithEmail } from "./authService"; type LoginScreenProps = { + initialNotice?: string; onCreateAccount: () => void; onLogin: () => void; }; -const INVALID_MESSAGE = "Invalid username or password."; +const INVALID_MESSAGE = "Invalid email or password."; +const SERVICE_MESSAGE = + "Authentication is temporarily unavailable. Try again later."; -export function LoginScreen({ onCreateAccount, onLogin }: LoginScreenProps) { - const [username, setUsername] = useState(""); +export function LoginScreen({ + initialNotice, + onCreateAccount, + onLogin, +}: LoginScreenProps) { + const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); + const [notice, setNotice] = useState(initialNotice ?? ""); const [loading, setLoading] = useState(false); async function handleLogin() { - const trimmedUsername = username.trim(); + const trimmedEmail = email.trim().toLowerCase(); - if (!trimmedUsername || !password) { - setError("Enter your username and password to continue."); + if (!trimmedEmail || !password) { + setError("Enter your email and password to continue."); return; } setError(""); + setNotice(""); setLoading(true); - // 1. Look up the email that belongs to this username - const { data: email, error: lookupError } = await supabase.rpc( - "get_email_for_username", - { p_username: trimmedUsername }, - ); - - if (lookupError) { - console.log("get_email_for_username error:", lookupError); + try { + const result = await signInWithEmail(trimmedEmail, password); + + if (!result.ok) { + setError( + result.failure.kind === "network" + ? "Unable to connect. Try again." + : result.failure.kind === "credentials" + ? INVALID_MESSAGE + : SERVICE_MESSAGE, + ); + return; + } + + onLogin(); + } finally { setLoading(false); - setError("Something went wrong. Try again."); - return; } - - if (!email) { - setLoading(false); - setError(INVALID_MESSAGE); // same message on purpose - return; - } - - // 2. Sign in with that email + the password - const { error: signInError } = await supabase.auth.signInWithPassword({ - email, - password, - }); - setLoading(false); - - if (signInError) { - console.log("signIn error:", signInError.message); - setError(INVALID_MESSAGE); - return; - } - - onLogin(); } return ( @@ -87,17 +81,23 @@ export function LoginScreen({ onCreateAccount, onLogin }: LoginScreenProps) { Log in to continue to QuickCop. + {notice ? ( + + {notice} + + ) : null} - Username + Email diff --git a/apps/mobile/features/auth/authService.ts b/apps/mobile/features/auth/authService.ts new file mode 100644 index 0000000..0dbf560 --- /dev/null +++ b/apps/mobile/features/auth/authService.ts @@ -0,0 +1,151 @@ +import { supabase } from "../../lib/supabase"; + +export type AuthFailureKind = + | "configuration" + | "credentials" + | "network" + | "unknown"; + +export type AuthFailure = { + kind: AuthFailureKind; +}; + +export const AUTH_REQUEST_TIMEOUT_MS = 10_000; + +export async function withTimeout( + operation: Promise, + timeoutMs = AUTH_REQUEST_TIMEOUT_MS, + message = "Authentication request timed out.", +): Promise { + let timeoutId: ReturnType | undefined; + const timeout = new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs); + }); + + try { + return await Promise.race([operation, timeout]); + } finally { + if (timeoutId !== undefined) clearTimeout(timeoutId); + } +} + +export type SignInResult = { ok: true } | { ok: false; failure: AuthFailure }; + +export type SignUpResult = + | { ok: true; sessionCreated: boolean } + | { ok: false; failure: AuthFailure }; + +function isNetworkFailure(error: unknown) { + return ( + error instanceof TypeError || + (error instanceof Error && + /network|fetch|timeout|offline/i.test(error.message)) + ); +} + +function classifyProviderFailure(error: unknown): AuthFailure { + if (isNetworkFailure(error)) { + return { kind: "network" }; + } + + if ( + error instanceof Error && + /invalid.*(login|credential)|credential.*invalid/i.test(error.message) + ) { + return { kind: "credentials" }; + } + + if ( + error && + typeof error === "object" && + "status" in error && + (error.status === 400 || error.status === 401) + ) { + return { kind: "credentials" }; + } + + return { kind: "unknown" }; +} + +export async function signInWithEmail( + email: string, + password: string, +): Promise { + if (!supabase) { + return { ok: false, failure: { kind: "configuration" } }; + } + + try { + const { error } = await withTimeout( + supabase.auth.signInWithPassword({ email, password }), + ); + + if (error) { + return { ok: false, failure: classifyProviderFailure(error) }; + } + + return { ok: true }; + } catch (error) { + return { ok: false, failure: classifyProviderFailure(error) }; + } +} + +export async function signUpWithEmail(input: { + email: string; + password: string; + username: string; + phone: string | null; +}): Promise { + if (!supabase) { + return { ok: false, failure: { kind: "configuration" } }; + } + + try { + const { data, error } = await withTimeout( + supabase.auth.signUp({ + email: input.email, + password: input.password, + options: { + data: { + username: input.username, + display_name: input.username, + phone: input.phone, + }, + }, + }), + ); + + if (error) { + return { ok: false, failure: classifyProviderFailure(error) }; + } + + return { ok: true, sessionCreated: Boolean(data.session) }; + } catch (error) { + return { ok: false, failure: classifyProviderFailure(error) }; + } +} + +export async function signOut(): Promise<{ ok: boolean }> { + if (!supabase) return { ok: false }; + + const remoteLogout = withTimeout<{ error: unknown }>( + supabase.auth.signOut({ scope: "global" }), + AUTH_REQUEST_TIMEOUT_MS, + "Remote logout timed out.", + ).catch(() => ({ error: new Error("Remote logout failed.") })); + + let localLogoutSucceeded = false; + try { + const { error } = await withTimeout<{ error: unknown }>( + supabase.auth.signOut({ scope: "local" }), + 2_000, + "Local logout timed out.", + ); + localLogoutSucceeded = !error; + } catch { + localLogoutSucceeded = false; + } + + const { error: remoteError } = await remoteLogout; + return { ok: localLogoutSucceeded && !remoteError }; +} diff --git a/apps/mobile/features/home/HomeScreen.tsx b/apps/mobile/features/home/HomeScreen.tsx index 5ce3be6..06fab4b 100644 --- a/apps/mobile/features/home/HomeScreen.tsx +++ b/apps/mobile/features/home/HomeScreen.tsx @@ -3,11 +3,9 @@ import { useState } from "react"; import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import { supabase } from "../../lib/supabase"; - type HomeScreenProps = { onAccountInfo?: () => void; - onLogout: () => void; + onLogout: () => void | Promise; onOpenNotifications: () => void; onOpenWatchedItems?: () => void; }; @@ -23,8 +21,7 @@ export function HomeScreen({ const [storesOpen, setStoresOpen] = useState(false); async function handleLogout() { - await supabase.auth.signOut(); - onLogout(); + await onLogout(); } return ( diff --git a/apps/mobile/lib/secureStorage.ts b/apps/mobile/lib/secureStorage.ts new file mode 100644 index 0000000..82746c9 --- /dev/null +++ b/apps/mobile/lib/secureStorage.ts @@ -0,0 +1,7 @@ +import * as SecureStore from "expo-secure-store"; + +export const secureStorage = { + getItem: (key: string) => SecureStore.getItemAsync(key), + setItem: (key: string, value: string) => SecureStore.setItemAsync(key, value), + removeItem: (key: string) => SecureStore.deleteItemAsync(key), +}; diff --git a/apps/mobile/lib/supabase.ts b/apps/mobile/lib/supabase.ts index 58a6b50..7264200 100644 --- a/apps/mobile/lib/supabase.ts +++ b/apps/mobile/lib/supabase.ts @@ -1,16 +1,33 @@ import "react-native-url-polyfill/auto"; -import AsyncStorage from "@react-native-async-storage/async-storage"; -import { createClient } from "@supabase/supabase-js"; +import { createClient, type SupabaseClient } from "@supabase/supabase-js"; -export const supabase = createClient( - process.env.EXPO_PUBLIC_SUPABASE_URL!, - process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!, - { - auth: { - storage: AsyncStorage, - autoRefreshToken: true, - persistSession: true, - detectSessionInUrl: false, - }, - }, -); +import { secureStorage } from "./secureStorage"; +import { + readSupabaseConfig, + type SupabaseConfig, + SupabaseConfigurationError, +} from "./supabaseConfig"; + +let config: SupabaseConfig | null = null; +export let supabaseConfigError: SupabaseConfigurationError | null = null; + +try { + config = readSupabaseConfig(); +} catch (error) { + if (error instanceof SupabaseConfigurationError) { + supabaseConfigError = error; + } else { + throw error; + } +} + +export const supabase: SupabaseClient | null = config + ? createClient(config.url, config.publishableKey, { + auth: { + storage: secureStorage, + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: false, + }, + }) + : null; diff --git a/apps/mobile/lib/supabaseConfig.ts b/apps/mobile/lib/supabaseConfig.ts new file mode 100644 index 0000000..ea8fe9e --- /dev/null +++ b/apps/mobile/lib/supabaseConfig.ts @@ -0,0 +1,93 @@ +export type SupabaseConfig = { + url: string; + publishableKey: string; +}; + +export class SupabaseConfigurationError extends Error { + readonly isPublicDiagnostic: boolean; + + constructor(message: string, isPublicDiagnostic: boolean) { + super(message); + this.name = "SupabaseConfigurationError"; + this.isPublicDiagnostic = isPublicDiagnostic; + } +} + +function isDevelopmentBuild() { + return ( + process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test" + ); +} + +function isServiceRoleJwt(value: string) { + const payload = value.split(".")[1]; + if (!payload || typeof globalThis.atob !== "function") return false; + + try { + const normalizedPayload = payload + .replaceAll("-", "+") + .replaceAll("_", "/") + .padEnd(Math.ceil(payload.length / 4) * 4, "="); + const decoded = JSON.parse(globalThis.atob(normalizedPayload)) as { + role?: unknown; + }; + return decoded.role === "service_role"; + } catch { + return false; + } +} + +export function readSupabaseConfig( + environment: Record = process.env, +): SupabaseConfig { + const url = environment.EXPO_PUBLIC_SUPABASE_URL?.trim(); + const publishableKey = ( + environment.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY ?? + environment.EXPO_PUBLIC_SUPABASE_ANON_KEY + )?.trim(); + const publicDiagnostic = isDevelopmentBuild(); + + if (!url || !publishableKey) { + throw new SupabaseConfigurationError( + publicDiagnostic + ? "Missing EXPO_PUBLIC_SUPABASE_URL or EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY." + : "Authentication is unavailable.", + publicDiagnostic, + ); + } + + let parsedUrl: URL; + try { + parsedUrl = new URL(url); + } catch { + throw new SupabaseConfigurationError( + publicDiagnostic + ? "EXPO_PUBLIC_SUPABASE_URL must be a valid URL." + : "Authentication is unavailable.", + publicDiagnostic, + ); + } + + const localDevelopmentUrl = + publicDiagnostic && + ["localhost", "127.0.0.1", "10.0.2.2"].includes(parsedUrl.hostname); + if (parsedUrl.protocol !== "https:" && !localDevelopmentUrl) { + throw new SupabaseConfigurationError( + publicDiagnostic + ? "EXPO_PUBLIC_SUPABASE_URL must use HTTPS outside local development." + : "Authentication is unavailable.", + publicDiagnostic, + ); + } + + if (isServiceRoleJwt(publishableKey)) { + throw new SupabaseConfigurationError( + publicDiagnostic + ? "The public Supabase key must not contain a service-role key." + : "Authentication is unavailable.", + publicDiagnostic, + ); + } + + return { url, publishableKey }; +} diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json index db08a3a..c9a42b9 100644 --- a/apps/mobile/package-lock.json +++ b/apps/mobile/package-lock.json @@ -10,7 +10,6 @@ "dependencies": { "@expo/metro-runtime": "~57.0.16", "@expo/ui": "~57.0.9", - "@react-native-async-storage/async-storage": "2.2.0", "@supabase/supabase-js": "^2.117.3", "expo": "~57.0.10", "expo-constants": "~57.0.9", @@ -20,6 +19,7 @@ "expo-image": "~57.0.2", "expo-linking": "~57.0.5", "expo-router": "~57.0.10", + "expo-secure-store": "~15.0.7", "expo-splash-screen": "~57.0.5", "expo-status-bar": "~57.0.1", "expo-symbols": "~57.0.1", @@ -37,6 +37,7 @@ "react-native-worklets": "0.10.1" }, "devDependencies": { + "@react-native/jest-preset": "0.86.2", "@testing-library/react-native": "^14.0.1", "@types/jest": "^29.5.14", "@types/react": "~19.2.2", @@ -2698,18 +2699,6 @@ } } }, - "node_modules/@react-native-async-storage/async-storage": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", - "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==", - "license": "MIT", - "dependencies": { - "merge-options": "^3.0.4" - }, - "peerDependencies": { - "react-native": "^0.0.0-0 || >=0.65 <1.0" - } - }, "node_modules/@react-native-masked-view/masked-view": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@react-native-masked-view/masked-view/-/masked-view-0.3.2.tgz", @@ -2936,7 +2925,6 @@ "integrity": "sha512-wneoqwKWdv6wIcWotVgp6NMlMWcJDjxDnp7fVYym2Pn6JLgAKgkbmuHGmxW0cLCGoa9wtcO680uciv+Kzx0G7w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/js-polyfills": "0.86.2", @@ -5464,6 +5452,15 @@ } } }, + "node_modules/expo-secure-store": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-15.0.8.tgz", + "integrity": "sha512-lHnzvRajBu4u+P99+0GEMijQMFCOYpWRO4dWsXSuMt77+THPIGjzNvVKrGSl6mMrLsfVaKL8BpwYZLGlgA+zAw==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-server": { "version": "57.0.3", "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-57.0.3.tgz", @@ -6080,6 +6077,7 @@ "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, @@ -6603,15 +6601,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -8216,9 +8205,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8239,9 +8225,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8262,9 +8245,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8285,9 +8265,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8544,18 +8521,6 @@ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" }, - "node_modules/merge-options": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", - "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", - "license": "MIT", - "dependencies": { - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 5d1444f..5d3328c 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -17,7 +17,6 @@ "dependencies": { "@expo/metro-runtime": "~57.0.16", "@expo/ui": "~57.0.9", - "@react-native-async-storage/async-storage": "2.2.0", "@supabase/supabase-js": "^2.117.3", "expo": "~57.0.10", "expo-constants": "~57.0.9", @@ -27,6 +26,7 @@ "expo-image": "~57.0.2", "expo-linking": "~57.0.5", "expo-router": "~57.0.10", + "expo-secure-store": "~15.0.7", "expo-splash-screen": "~57.0.5", "expo-status-bar": "~57.0.1", "expo-symbols": "~57.0.1", @@ -44,6 +44,7 @@ "react-native-worklets": "0.10.1" }, "devDependencies": { + "@react-native/jest-preset": "0.86.2", "@testing-library/react-native": "^14.0.1", "@types/jest": "^29.5.14", "@types/react": "~19.2.2", @@ -53,6 +54,7 @@ "typescript": "~6.0.3" }, "jest": { - "preset": "jest-expo" + "preset": "jest-expo", + "testTimeout": 30000 } } diff --git a/infrastructure_plan.md b/infrastructure_plan.md index 547f8e1..51f7ca5 100644 --- a/infrastructure_plan.md +++ b/infrastructure_plan.md @@ -19,7 +19,7 @@ - **Backend required:** Yes. It runs approved partner stock-monitoring integrations, sends notifications, and enforces authorization. - **Cross-device persistence:** Hosted, per-account data. - **Interaction between accounts:** None. -- **Primary application components:** Expo Android client, NestJS API and worker process, PostgreSQL, notification provider, payment/wallet integration, and approved partner-retailer APIs. +- **Primary application components:** Expo Android client, NestJS API and worker process, Supabase-managed PostgreSQL, notification provider, payment/wallet integration, and approved partner-retailer APIs. - **Checkout boundary:** Only participating retailers with approved checkout APIs are supported. The app must never automate arbitrary retailer sites or retain raw payment credentials. ## 3. Selected Technology Stack @@ -37,23 +37,23 @@ ## 4. Storage and Persistence - **Storage model:** Hosted relational storage. -- **Primary data store:** PostgreSQL for accounts, tracked product URLs/identifiers, availability history, notification preferences, and encrypted delivery addresses. +- **Primary data store:** Supabase-managed PostgreSQL for accounts, tracked product URLs/identifiers, availability history, notification preferences, and encrypted delivery addresses. - **User files or object storage:** Not planned initially; do not persist payment or identity-document files. -- **Local-development storage:** PostgreSQL Docker service with disposable developer data. -- **Production hosting model:** Cloud SQL for PostgreSQL with private IP, automated backups, and tested restores. Cloud Run API and worker services connect through private networking with least-privilege service accounts. -- **Schema and migration approach:** Versioned, reviewed migrations executed once by a controlled release job. -- **Backup, export, or recovery approach:** Encrypted scheduled backups, tested restores, per-user account/address export and deletion capability, and retention periods approved by counsel. -- **Secrets and connection-string approach:** Secret manager/environment injection only; no credentials in source, images, logs, or mobile clients. +- **Local-development storage:** Supabase local development stack managed through the Supabase CLI and Docker, with disposable developer data; Testcontainers may still provide isolated PostgreSQL databases for integration tests. +- **Production hosting model:** A dedicated Supabase project provides the application PostgreSQL database, managed backups, connection pooling, and private service credentials. Cloud Run API and worker services connect using least-privilege Supabase database credentials over TLS. +- **Schema and migration approach:** Versioned, reviewed Supabase/PostgreSQL migrations executed once by a controlled release job through the Supabase CLI or migration tooling. +- **Backup, export, or recovery approach:** Supabase backups and project recovery capabilities, supplemented by tested exports/restores, plus per-user account/address export and deletion capability and retention periods approved by counsel. +- **Secrets and connection-string approach:** Store the Supabase project URL, publishable client key, server-only service-role key, and database connection credentials in the platform secret manager/GitHub protected environments; no credentials in source, images, logs, or mobile clients. - **Payment and address handling:** Do not collect, store, log, or transmit PAN, CVV, or wallet credentials. Use partner-approved Apple Pay/Google Pay, Stripe Link, or equivalent provider flows; retain only provider tokens and minimal transaction references when permitted. Encrypt saved delivery addresses at rest and in transit, authorize per user, audit privileged access, and delete on account deletion. -- **Reason this storage fits the access pattern:** PostgreSQL supports secure per-account data, transactional alert state, and fast partner checkout preparation. +- **Reason this storage fits the access pattern:** Supabase provides managed PostgreSQL for secure per-account data, transactional alert state, and fast partner checkout preparation without operating a separate managed database. ## 5. Testing Tools -| Test layer | Tool or library | Planned scope | Planned execution point | -| ---------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------- | -| Unit | Jest and React Native Testing Library | Mobile UI, validation, notification presentation | Local and pull requests | -| Integration | Jest/NestJS test utilities and Testcontainers PostgreSQL | Auth, authorization, encrypted address access, partner adapters, stock transitions | Local and pull requests | -| End-to-end or UI | Maestro | Android onboarding, alert receipt, and approved checkout handoff | Pull requests when practical; release candidate | +| Test layer | Tool or library | Planned scope | Planned execution point | +| ---------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------- | +| Unit | Jest and React Native Testing Library | Mobile UI, validation, notification presentation | Local and pull requests | +| Integration | Jest/NestJS test utilities, Testcontainers PostgreSQL, and Supabase test configuration | Auth, authorization, encrypted address access, partner adapters, stock transitions | Local and pull requests | +| End-to-end or UI | Maestro | Android onboarding, alert receipt, and approved checkout handoff | Pull requests when practical; release candidate | ## 6. Test Analysis @@ -67,48 +67,49 @@ ## 7. Static Analysis and Security -| Check | Tool | Planned enforcement | -| ---------------------------------------- | ------------------------------------------------- | -------------------------------------------------------------- | -| Formatting | Prettier | Verify on pull requests | -| Linting | ESLint | Block pull requests | -| Type checking or compiler warnings | TypeScript compiler | Block pull requests with strict settings | -| Anti-pattern or maintainability analysis | ESLint rules and Semgrep | Block high-confidence findings; tune reviewed false positives | -| Dependency vulnerability scanning | Dependabot and npm audit/compatible advisory scan | Dependabot updates; block actionable high/critical findings | -| Secret scanning | GitHub secret scanning and Gitleaks | Block detected secrets; rotate exposed credentials immediately | -| Static security analysis | CodeQL and Semgrep | CodeQL scheduled and on relevant PRs; Semgrep on PRs | -| Container scanning | Trivy | Scan production API and PostgreSQL images before release | +| Check | Tool | Planned enforcement | +| ---------------------------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------- | +| Formatting | Prettier | Verify on pull requests | +| Linting | ESLint | Block pull requests | +| Type checking or compiler warnings | TypeScript compiler | Block pull requests with strict settings | +| Anti-pattern or maintainability analysis | ESLint rules and Semgrep | Block high-confidence findings; tune reviewed false positives | +| Dependency vulnerability scanning | Dependabot and npm audit/compatible advisory scan | Dependabot updates; block actionable high/critical findings | +| Secret scanning | GitHub secret scanning and Gitleaks | Block detected secrets; rotate exposed credentials immediately | +| Static security analysis | CodeQL and Semgrep | CodeQL scheduled and on relevant PRs; Semgrep on PRs | +| Container scanning | Trivy | Scan the production API/worker image and any released supporting images before release | ## 8. Development Technologies Requiring Manual Installation These are developer-workstation prerequisites that will not be supplied by the planned Docker environment. -| Technology | Why it is needed | Required on which machines | Version policy | Planned installation or verification method | Why Docker does not provide it | -| ----------------------------------------------------- | --------------------------------------------- | -------------------------------------------- | --------------------------------- | ------------------------------------------- | ---------------------------------------------------- | -| Git | Source control | All developer machines | Supported stable | Future documented setup check | Developer host tool | -| Node.js LTS and npm | Expo tooling and local client development | All developer machines | Supported LTS | Future version check | Mobile tooling runs on host | -| Android Studio and Android SDK | Emulator, debugging, and Android builds | Android app developers | Play-compatible supported version | Future documented setup check | Requires host emulator/device integration | -| Java JDK required by Gradle | Android builds | Android app developers and Android CI runner | Expo/Gradle-compatible LTS | Future documented setup check | Native Android toolchain dependency | -| Docker Desktop/Engine | Planned local backend and database containers | Backend developers | Supported stable | Future documented setup check | It is the local container runtime | -| Google Play developer account and signing credentials | Store publishing | Release owner/secure CI | Current Play requirements | Obtain before release | Account-held credentials cannot be baked into Docker | +| Technology | Why it is needed | Required on which machines | Version policy | Planned installation or verification method | Why Docker does not provide it | +| ----------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------- | --------------------------------- | ------------------------------------------- | ---------------------------------------------------- | +| Git | Source control | All developer machines | Supported stable | Future documented setup check | Developer host tool | +| Node.js LTS and npm | Expo tooling and local client development | All developer machines | Supported LTS | Future version check | Mobile tooling runs on host | +| Android Studio and Android SDK | Emulator, debugging, and Android builds | Android app developers | Play-compatible supported version | Future documented setup check | Requires host emulator/device integration | +| Java JDK required by Gradle | Android builds | Android app developers and Android CI runner | Expo/Gradle-compatible LTS | Future documented setup check | Native Android toolchain dependency | +| Supabase CLI | Start the local Supabase stack and apply reviewed migrations | Backend developers and release CI | Supported stable | Future documented setup check | The CLI coordinates Supabase services and migrations | +| Docker Desktop/Engine | Runs local NestJS and Supabase services | Backend developers | Supported stable | Future documented setup check | It is the local container runtime | +| Google Play developer account and signing credentials | Store publishing | Release owner/secure CI | Current Play requirements | Obtain before release | Account-held credentials cannot be baked into Docker | ### Host tools intentionally not required -- **Not required because Docker supplies them:** Production-like NestJS runtime and local PostgreSQL service. +- **Not required because Docker supplies them:** Production-like NestJS runtime and the Supabase local service containers. - **Not required for this platform:** Xcode, iOS SDK, Apple signing/notarization tools, and desktop packaging SDKs. ## 9. Docker Plan -- **Planned Docker role:** Local development for the NestJS backend and PostgreSQL; containerized API/worker deployment to Cloud Run. -- **Future files that would be created during implementation:** API Dockerfile, PostgreSQL service definition, Compose configuration, `.dockerignore`, and environment templates. -- **Planned images and services:** Local multi-stage non-root NestJS API/worker image and version-pinned PostgreSQL image; Cloud Run deploys the API/worker image and Cloud SQL supplies production PostgreSQL. -- **Development container behavior:** API and database run as separate services; source bind mount only for development; database data uses a named volume. -- **Ports:** Bind database only to the internal Docker network; expose API only through a TLS-capable reverse proxy/gateway. -- **Bind mounts and named volumes:** Named persistent PostgreSQL volume; avoid mounting host secrets or production data. +- **Planned Docker role:** Local development for the NestJS backend and Supabase local services; containerized API/worker deployment to Cloud Run. +- **Future files that would be created during implementation:** API Dockerfile, Supabase local configuration, Compose/Supabase CLI configuration, `.dockerignore`, and environment templates. +- **Planned images and services:** Local multi-stage non-root NestJS API/worker image alongside the Supabase local stack; Cloud Run deploys the API/worker image and the dedicated Supabase project supplies production PostgreSQL. +- **Development container behavior:** API and Supabase local services run as separate services; source bind mount only for development; local database data is disposable and never reused for production. +- **Ports:** Bind local Supabase services and the API only to localhost or the local development network; expose production API only through a TLS-capable reverse proxy/gateway. +- **Bind mounts and named volumes:** Use Supabase-managed local volumes only for disposable development data; avoid mounting host secrets or production data. - **Environment-variable and secret handling:** Inject runtime secrets from the platform secret manager; never copy `.env` files or credentials into images. -- **Local database or service containers:** PostgreSQL only; use Testcontainers for isolated integration-test databases. -- **Production image or non-container release path:** Publish scanned API/worker image to Artifact Registry and deploy separate Cloud Run API and worker services with `min-instances` at least 1; use Cloud SQL private IP. Android artifact is built on a native Android runner and published separately. +- **Local database or service containers:** Supabase local stack for development; use Testcontainers for isolated integration-test databases. +- **Production image or non-container release path:** Publish scanned API/worker image to Artifact Registry and deploy separate Cloud Run API and worker services with `min-instances` at least 1; connect to the dedicated Supabase project over TLS. Android artifact is built on a native Android runner and published separately. - **Build stages and hardening:** Multi-stage build, minimal runtime base, non-root users, `.dockerignore`, pinned bases, health checks, read-only filesystem where compatible, and no embedded secrets. -- **Planned future development command:** `docker compose up` (future only; do not run during planning). +- **Planned future development command:** `supabase start` (future only; do not run during planning). - **Planned future production command:** Deployment-platform-specific image rollout command (future only; select before implementation). ## 10. GitHub Actions Plan @@ -137,7 +138,7 @@ These are developer-workstation prerequisites that will not be supplied by the p - **Future workflow file:** `.github/workflows/release.yml` - **Release trigger:** Protected `v*` tag or `workflow_dispatch` with an explicit release version. -- **Release destination:** Google Play Store production track, Cloud Run API and worker services (each `min-instances` ≥ 1), and Cloud SQL for PostgreSQL with private IP and automated backups. +- **Release destination:** Google Play Store production track, Cloud Run API and worker services (each `min-instances` ≥ 1), and a dedicated Supabase project providing the production PostgreSQL database with managed backups. - **Runner or matrix:** Ubuntu for container build/scan; Android-compatible native runner for signed Android artifact. - **Planned jobs in order:** 1. Re-run lockfile install, static checks, tests, coverage gate, Android/API build validation, and container scan. @@ -153,18 +154,21 @@ These are developer-workstation prerequisites that will not be supplied by the p ### GitHub configuration required later -| Name | Type | Purpose | -| ------------------------------------------- | ---------------------------- | ---------------------------------------------------------------- | -| `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON` | Secret | Authenticate CI publishing to Google Play | -| `ANDROID_SIGNING_KEY` and signing passwords | Secrets | Sign Android release artifacts | -| `GCP_WORKLOAD_IDENTITY_PROVIDER` | GitHub variable | Google Cloud Workload Identity Federation provider resource name | -| `GCP_SERVICE_ACCOUNT` | GitHub variable | Least-privilege deployer service-account email | -| `GCP_PROJECT_ID` and `GCP_REGION` | GitHub variables | Target Artifact Registry, Cloud Run, and Cloud SQL location | -| `CLOUD_SQL_INSTANCE` | GitHub variable | Production Cloud SQL instance connection identifier | -| `DATABASE_URL` and encryption-key material | Secrets | Connect securely and encrypt protected address fields | -| Partner API credentials | Secrets | Access approved inventory and checkout integrations | -| `production` | Protected GitHub environment | Require approval and scope production secrets | -| Google Play Console account | Account | Own Android distribution and release controls | +| Name | Type | Purpose | +| ---------------------------------------------------- | ---------------------------- | ----------------------------------------------------------------- | +| `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON` | Secret | Authenticate CI publishing to Google Play | +| `ANDROID_SIGNING_KEY` and signing passwords | Secrets | Sign Android release artifacts | +| `GCP_WORKLOAD_IDENTITY_PROVIDER` | GitHub variable | Google Cloud Workload Identity Federation provider resource name | +| `GCP_SERVICE_ACCOUNT` | GitHub variable | Least-privilege deployer service-account email | +| `GCP_PROJECT_ID` and `GCP_REGION` | GitHub variables | Target Artifact Registry and Cloud Run location | +| `SUPABASE_PROJECT_REF` and `SUPABASE_URL` | GitHub variables | Identify the dedicated Supabase project and API endpoint | +| `SUPABASE_PUBLISHABLE_KEY` | Variable | Configure the mobile client with Supabase's non-secret client key | +| `SUPABASE_ACCESS_TOKEN` | Secret | Authenticate protected Supabase CLI migration operations | +| `SUPABASE_SERVICE_ROLE_KEY` and database credentials | Secrets | Server-side Supabase access and controlled migration connectivity | +| Encryption-key material | Secret | Encrypt protected address fields | +| Partner API credentials | Secrets | Access approved inventory and checkout integrations | +| `production` | Protected GitHub environment | Require approval and scope production secrets | +| Google Play Console account | Account | Own Android distribution and release controls | ## 11. Planned Repository Artifacts - Not Created by This Skill @@ -179,5 +183,5 @@ These are developer-workstation prerequisites that will not be supplied by the p ## 12. Assumptions and Open Items - **Assumptions:** First launch is U.S.-only; only partner retailers with approved APIs are supported; Android is the sole initial client; user confirmation via a supported wallet/provider remains required for payment authorization. -- **Decisions still requiring an external account, credential, certificate, or organizational approval:** Partner agreements/API access, payment-provider onboarding, Google Cloud project and Workload Identity Federation setup, Cloud Run/Cloud SQL service accounts, Google Play developer account, Android signing, push-notification provider, encryption-key management, and legal/privacy review. +- **Decisions still requiring an external account, credential, certificate, or organizational approval:** Supabase organization/project and backup plan, partner agreements/API access, payment-provider onboarding, Google Cloud project and Workload Identity Federation setup, Cloud Run service accounts, Google Play developer account, Android signing, push-notification provider, encryption-key management, and legal/privacy review. - **Items to confirm before implementation begins:** Exact supported retailers and checkout contracts; provider for Android wallet/payment authorization; target Android OS range; address retention period; privacy policy, consent, deletion/export flow, incident response, and applicable U.S./California legal obligations. From 9d343ec8416f61765d1a0741ee4cb6f394e50243 Mon Sep 17 00:00:00 2001 From: InsertUzername Date: Mon, 19 Oct 2026 07:51:16 -0700 Subject: [PATCH 06/12] Add account tables migration, NestJS account API, and e2e tests --- apps/api/.prettierrc | 4 + apps/api/README.md | 98 + apps/api/eslint.config.mjs | 35 + apps/api/nest-cli.json | 8 + apps/api/package-lock.json | 10446 ++++++++++++++++ apps/api/package.json | 75 + apps/api/src/account/account.controller.ts | 25 + apps/api/src/account/account.module.ts | 10 + apps/api/src/account/account.service.ts | 75 + apps/api/src/account/dto.ts | 21 + apps/api/src/app.module.ts | 8 + apps/api/src/app.setup.ts | 8 + apps/api/src/auth/supabase-auth.guard.ts | 27 + apps/api/src/main.ts | 10 + apps/api/src/supabase/supabase.module.ts | 6 + apps/api/src/supabase/supabase.service.ts | 18 + apps/api/test/account.e2e-spec.ts | 156 + apps/api/test/jest-e2e.json | 10 + apps/api/test/setup-env.ts | 2 + apps/api/tsconfig.build.json | 4 + apps/api/tsconfig.json | 25 + supabase/.gitignore | 8 + supabase/config.toml | 415 + .../20261019073253_create_account_tables.sql | 51 + 24 files changed, 11545 insertions(+) create mode 100644 apps/api/.prettierrc create mode 100644 apps/api/README.md create mode 100644 apps/api/eslint.config.mjs create mode 100644 apps/api/nest-cli.json create mode 100644 apps/api/package-lock.json create mode 100644 apps/api/package.json create mode 100644 apps/api/src/account/account.controller.ts create mode 100644 apps/api/src/account/account.module.ts create mode 100644 apps/api/src/account/account.service.ts create mode 100644 apps/api/src/account/dto.ts create mode 100644 apps/api/src/app.module.ts create mode 100644 apps/api/src/app.setup.ts create mode 100644 apps/api/src/auth/supabase-auth.guard.ts create mode 100644 apps/api/src/main.ts create mode 100644 apps/api/src/supabase/supabase.module.ts create mode 100644 apps/api/src/supabase/supabase.service.ts create mode 100644 apps/api/test/account.e2e-spec.ts create mode 100644 apps/api/test/jest-e2e.json create mode 100644 apps/api/test/setup-env.ts create mode 100644 apps/api/tsconfig.build.json create mode 100644 apps/api/tsconfig.json create mode 100644 supabase/.gitignore create mode 100644 supabase/config.toml create mode 100644 supabase/migrations/20261019073253_create_account_tables.sql diff --git a/apps/api/.prettierrc b/apps/api/.prettierrc new file mode 100644 index 0000000..a20502b --- /dev/null +++ b/apps/api/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} diff --git a/apps/api/README.md b/apps/api/README.md new file mode 100644 index 0000000..8f0f65f --- /dev/null +++ b/apps/api/README.md @@ -0,0 +1,98 @@ +

+ Nest Logo +

+ +[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 +[circleci-url]: https://circleci.com/gh/nestjs/nest + +

A progressive Node.js framework for building efficient and scalable server-side applications.

+

+NPM Version +Package License +NPM Downloads +CircleCI +Discord +Backers on Open Collective +Sponsors on Open Collective + Donate us + Support us + Follow us on Twitter +

+ + +## Description + +[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. + +## Project setup + +```bash +$ npm install +``` + +## Compile and run the project + +```bash +# development +$ npm run start + +# watch mode +$ npm run start:dev + +# production mode +$ npm run start:prod +``` + +## Run tests + +```bash +# unit tests +$ npm run test + +# e2e tests +$ npm run test:e2e + +# test coverage +$ npm run test:cov +``` + +## Deployment + +When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information. + +If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps: + +```bash +$ npm install -g @nestjs/mau +$ mau deploy +``` + +With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure. + +## Resources + +Check out a few resources that may come in handy when working with NestJS: + +- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework. +- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy). +- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/). +- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks. +- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com). +- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com). +- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs). +- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com). + +## Support + +Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). + +## Stay in touch + +- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec) +- Website - [https://nestjs.com](https://nestjs.com/) +- Twitter - [@nestframework](https://twitter.com/nestframework) + +## License + +Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). diff --git a/apps/api/eslint.config.mjs b/apps/api/eslint.config.mjs new file mode 100644 index 0000000..4e9f827 --- /dev/null +++ b/apps/api/eslint.config.mjs @@ -0,0 +1,35 @@ +// @ts-check +import eslint from '@eslint/js'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['eslint.config.mjs'], + }, + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + eslintPluginPrettierRecommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + sourceType: 'commonjs', + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn', + "prettier/prettier": ["error", { endOfLine: "auto" }], + }, + }, +); diff --git a/apps/api/nest-cli.json b/apps/api/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/apps/api/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/apps/api/package-lock.json b/apps/api/package-lock.json new file mode 100644 index 0000000..e85e697 --- /dev/null +++ b/apps/api/package-lock.json @@ -0,0 +1,10446 @@ +{ + "name": "api", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "api", + "version": "0.0.1", + "license": "UNLICENSED", + "dependencies": { + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "@nestjs/platform-express": "^11.0.1", + "@supabase/supabase-js": "^2.118.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.18.0", + "@nestjs/cli": "^11.0.0", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.1", + "@types/express": "^5.0.0", + "@types/jest": "^30.0.0", + "@types/node": "^24.0.0", + "@types/supertest": "^7.2.1", + "dotenv": "^18.0.8", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2", + "globals": "^17.0.0", + "jest": "^30.0.0", + "prettier": "^3.4.2", + "source-map-support": "^0.5.21", + "supertest": "^7.3.1", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.27.tgz", + "integrity": "sha512-3amNzoCVSKd7ah6l6lBQL4onwwJvqvam7FMoQBILrxtW5LB5ezh8gMSPuA4zJjKjoRzf9uoWdlzqv/84I52xZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": 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/@angular-devkit/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.27.tgz", + "integrity": "sha512-/PZmyAlb2NGWPikRRuiWLdfHQd8Wrx6lX4HqvTcaDhlU43M3T0ud4PH2T3QDp7BzHYY92xtD8iPxX2asg67G1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.27.tgz", + "integrity": "sha512-wHYH6SVXVykhLzovUHtYor3Nl4SpIiITi7r9DQDaKYUD4hpRBx25W6N9eGuakT9Vd5tV/x6wmvQFWQZQwFB7eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "@angular-devkit/schematics": "19.2.27", + "@inquirer/prompts": "7.3.2", + "ansi-colors": "4.1.3", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/prompts": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", + "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.2", + "@inquirer/confirm": "^5.1.6", + "@inquirer/editor": "^4.2.7", + "@inquirer/expand": "^4.0.9", + "@inquirer/input": "^4.1.6", + "@inquirer/number": "^3.0.9", + "@inquirer/password": "^4.0.9", + "@inquirer/rawlist": "^4.0.9", + "@inquirer/search": "^3.0.9", + "@inquirer/select": "^4.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@dual-bundle/import-meta-resolve": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz", + "integrity": "sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/JounQin" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.5.2.tgz", + "integrity": "sha512-e8sQC4pCMHPBrPX7Hk8TJr+DM5dkUnImSTNag8SEWFAFAF6xIz4AJvCKapnrdHnZxep4bxw4PhG+ZbimYBMyKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.5.1", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.5.1", + "jest-util": "30.5.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.5.2.tgz", + "integrity": "sha512-/jqvFWoJF7LV1a6AUpYMbMm+2yIYsHJfR474H0SCzr9k8tnO/jl9wRg6zSG6lxgf6EozNlCG4amFzWsbZSvvZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.5.2", + "@jest/pattern": "30.5.0", + "@jest/reporters": "30.5.2", + "@jest/test-result": "30.5.2", + "@jest/transform": "30.5.2", + "@jest/types": "30.5.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.5.1", + "jest-config": "30.5.2", + "jest-haste-map": "30.5.1", + "jest-message-util": "30.5.1", + "jest-regex-util": "30.5.0", + "jest-resolve": "30.5.1", + "jest-resolve-dependencies": "30.5.2", + "jest-runner": "30.5.2", + "jest-runtime": "30.5.2", + "jest-snapshot": "30.5.2", + "jest-util": "30.5.1", + "jest-validate": "30.5.1", + "jest-watcher": "30.5.2", + "pretty-format": "30.5.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.5.0.tgz", + "integrity": "sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.5.2.tgz", + "integrity": "sha512-nwFPOUMbmsjNNRCpnJpB9HbSZu4C07wDDds++5j+j1MezzPLxMPDdbdUVjr2o4RULDOValnDd70taPFI1EtxAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.5.2", + "@jest/types": "30.5.1", + "@types/node": "*", + "jest-mock": "30.5.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.5.2.tgz", + "integrity": "sha512-2E+1Pg6jJxbRjLNLpciC71iri/3qsKJL222/aXJEFwC0VdUSvfS+JZQQLEVjUYYc8t8pQu2MGE3cF2+bNTYRsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.5.2", + "jest-snapshot": "30.5.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.5.2.tgz", + "integrity": "sha512-d/HOLSrJUlBIl6n3LzOa7cjW3xdePm5H5gQw2lixZ/0h157l9sw3mLblzFqnPcTbhDzhRQdBOX3A3KcG73nm5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.5.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.5.2.tgz", + "integrity": "sha512-GKk0h0tKT5SpDPxDhPg3r9mm5s8/YCBzVNMOGdRiQXwUiMsjU10mVPlMaQstH6/cu50vOb8N9oQUMETHuwwxKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.5.1", + "@sinonjs/fake-timers": "^15.4.0", + "@types/node": "*", + "jest-message-util": "30.5.1", + "jest-mock": "30.5.2", + "jest-util": "30.5.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.5.0.tgz", + "integrity": "sha512-9/2VUPitAjmBzbvDvqrxmvB7BzWsBW0WmkkojX1ODuxX1NLGxx9gfaZpHB0z8DtJ9uhGNmZG/VXBhf8uO0OV8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.5.2.tgz", + "integrity": "sha512-62zfS+dL+M5aTNHXCNLyqD+j4oqxTIzDYrZbpqMP8Yn+gvlAGyzC6BSPNb1ZmFOakV1RtFYX/O3FCQXSWmQdgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.5.2", + "@jest/expect": "30.5.2", + "@jest/types": "30.5.1", + "jest-mock": "30.5.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.5.0.tgz", + "integrity": "sha512-HdNQYSdRTEBNrginaqzQtTjG0HRMfrra/z6Ok7uL3S87vSlarIVohEsJsSj5edu3MiHoHjAkvPROz5ZjoKai+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.5.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/react-is-19": { + "name": "react-is", + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.3.1.tgz", + "integrity": "sha512-RRieF6IyxQBkcK6pP8xOHprLTPW6QjRkbGdImtqNme+/hYVwCZkLQLdlZGOhUqXwkS9IEynaGvPJtv6XB/cz0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/reporters": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.5.2.tgz", + "integrity": "sha512-ev6E9iG73twQcfAfoTnviuYkr3yOrYbzAtMFCimv2fxHXPIp9PBt2u1wNMt0aYWgl8FPwIcS8oqRJg6alnht4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.5.2", + "@jest/test-result": "30.5.2", + "@jest/transform": "30.5.2", + "@jest/types": "30.5.1", + "@jridgewell/trace-mapping": "^0.3.31", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^13.0.6", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.5.1", + "jest-util": "30.5.1", + "jest-worker": "30.5.1", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.5.0.tgz", + "integrity": "sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.5.1.tgz", + "integrity": "sha512-V3wnxNtiVmw5PPVg433Cn3VdXnsOeu/ofLw3KC04Bn2y1wIlU5kozXQn48rhrgj/02LrCVtntTEF1yBha0XSUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.5.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.5.2.tgz", + "integrity": "sha512-c5CehvkbfHX6yyNg7bMn2ylqlp9AHXuZ/3hPH/Lh5bxD/vefd/lV9HBjMqXef9yQNqkdwyZvkLSVSwB38hy8Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "callsites": "^3.1.0", + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.5.2.tgz", + "integrity": "sha512-eO6YU5rsLfs60ne694e0IAmRgeBnoA8mu0nlxXDbl/1j5km0o2vO9/VbbUIKIH+WRKKEI+ELgwraRGn2Lhep8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.5.2", + "@jest/types": "30.5.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.5.2.tgz", + "integrity": "sha512-bmij8jZyYAC47ExT2GeP7PcrlwSNS+Bp3KeRuBH88gpcxqDv6fljN8PemS4J/lLZ79xb6mpaennQTTq0zUAVVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.5.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.5.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.5.2.tgz", + "integrity": "sha512-LBGwmaE886C41jDZ65eqR1YbXM7642MK3ZhTowZd5+xDlrDLuf4ePuDdDmVSvcr8NTVJ3MqxLkMbUCzuigUaoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.5.1", + "@jridgewell/trace-mapping": "^0.3.31", + "babel-plugin-istanbul": "^8.0.0", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.5.1", + "jest-regex-util": "30.5.0", + "jest-util": "30.5.1", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.5.1.tgz", + "integrity": "sha512-LvVYn83nnXPl+Rg98nvcFgjx6nRMTArhSn6RAX/w3ELn54S8A42TYZvsCMdGUqTM8S0wyXbtlQdU6Hi6dykj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.5.0", + "@jest/schemas": "30.5.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.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/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz", + "integrity": "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@nestjs/cli": { + "version": "11.0.24", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.24.tgz", + "integrity": "sha512-aIHxQLSYtXShifA3zwWIeznEsZnNa3Iz2QRykFj+sl9IcbERBHr5nH87FRgywM+He3NxoF5WazHfR8FsmVeWxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "@angular-devkit/schematics": "19.2.27", + "@angular-devkit/schematics-cli": "19.2.27", + "@inquirer/prompts": "7.10.1", + "@nestjs/schematics": "^11.0.1", + "ansis": "4.2.0", + "chokidar": "4.0.3", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.1.0", + "glob": "13.0.6", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.9.3", + "webpack": "5.106.2", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 20.11" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/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==", + "dev": 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/@nestjs/cli/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/@nestjs/cli/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nestjs/cli/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@nestjs/cli/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/cli/node_modules/webpack": { + "version": "5.106.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", + "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.1", + "mime-db": "^1.54.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/webpack/node_modules/schema-utils": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.8.1.tgz", + "integrity": "sha512-x39pYlJPyH7Uge5QO64B9XGP9zLDc9WNmiQZc+CE3IzZPvMu3lHOuAMGpYNoPixEL7MSWlGhYTg5UlLSL8G6iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0" + }, + "bin": { + "schema-utils": "bin/schema-utils.js" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.112.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/webpack/node_modules/webpack": { + "version": "5.112.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.112.0.tgz", + "integrity": "sha512-NIBpsRuCll036haDDt/CfcTDfsWNR9Z782nMRMI9D377BsGcYzZDTNU5TyzhCJzPIcihAxMBaMAhq2WSaDrH8Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.26.0", + "es-module-lexer": "^2.1.0", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.13.0", + "schema-utils": "^4.5.0", + "tapable": "^2.3.0", + "watchpack": "^2.6.1", + "webpack-sources": "^3.6.0" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@nestjs/common": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.2.7.tgz", + "integrity": "sha512-CGOa0I/5Ac43ydfpjztozOrvgDWYBdY9ktkO0OrRqRo1d8n5cphtrTGYHOwKLMIel0BpSYZoQymJ9Dywv/BzLw==", + "license": "MIT", + "dependencies": { + "file-type": "21.3.4", + "iterare": "1.2.1", + "load-esm": "1.0.3", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/core": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.2.7.tgz", + "integrity": "sha512-ynMkVOAEykJMtls1X+MQV3TNY6hrnMkyHowU8XY8R6UqM3tdqT4WNVtemQOmFrqHw3f85lbe765fcpZyjBOcqA==", + "license": "MIT", + "dependencies": { + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/platform-express": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.2.7.tgz", + "integrity": "sha512-+F0hV/4yJM+D/Qwyo5XmKMe6YGEb+ssT2jFWrgdFe1obumXCUeC1GNob/kNeXyzAXm7dcxZVUtmcoqaF6z36jA==", + "license": "MIT", + "dependencies": { + "cors": "2.8.6", + "express": "5.2.1", + "multer": "2.4.0", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0" + } + }, + "node_modules/@nestjs/schematics": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.1.0.tgz", + "integrity": "sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.24", + "@angular-devkit/schematics": "19.2.24", + "comment-json": "5.0.0", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "prettier": "^3.0.0", + "typescript": ">=4.8.2" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { + "version": "19.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.24.tgz", + "integrity": "sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { + "version": "19.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.24.tgz", + "integrity": "sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.24", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@nestjs/schematics/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": 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/@nestjs/schematics/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@nestjs/testing": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.2.7.tgz", + "integrity": "sha512-CoWN8pIjXPVhkbaT0JI1E7X/QOnf9A8cdrzuhU+9mzRS4296/kds6BT+4tH7kOH484tH3GWkkB0+K7dA/FVGKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "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/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.52", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", + "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@supabase/auth-js": { + "version": "2.118.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.118.0.tgz", + "integrity": "sha512-xWfPXyhS2KDSfAneMtI6p/tBnbdiz/5vaRKXEqUfzfC9WCniKr2Ds5o5G7RIGig0bom4z6wUY9zaV07WDVTfnQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.118.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.118.0.tgz", + "integrity": "sha512-4ofletPa/noKAU6KHeqsd3FztGzRPUEJRey1MKY0o+PIHaVePqRPShIXtl37gcVyIWaFM+zNg6r9zh0uUSFOSA==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/phoenix": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.5.tgz", + "integrity": "sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==", + "license": "MIT" + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.118.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.118.0.tgz", + "integrity": "sha512-JXy+37KPApHsyotaQACovv/Jy9E22rfiWLRuT+2UjoEAcAIelDFIrsF/FY47Dy19mO7lI0Tq+zDCX8oJ2Ozfiw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.118.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.118.0.tgz", + "integrity": "sha512-pzCIEWjqD5/xqEZQerpt+q9rsgYkcrkgCJgf0W2ULqC4fYUeB/p9CFa3t9vn+IVh6dMN9r4ZCE262lZvUC+Bfw==", + "license": "MIT", + "dependencies": { + "@supabase/phoenix": "0.4.5", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.118.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.118.0.tgz", + "integrity": "sha512-5MNkKt2Moy7Jud/sPX7/28BwdDxZMCVHGJT00b8op3KRa9LKSjOAYU5IcfGWIstaasAqY8GkbA6oI3IRDgB1YQ==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.118.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.118.0.tgz", + "integrity": "sha512-FxI8UXMVC2UG7IkeiI8fuQZqq2oI3JeSuN8MPIC2RLVaoEAxKS0nqE6kii9Ewpq8f9gNPM6git1Y8f0vN/QLFw==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.118.0", + "@supabase/functions-js": "2.118.0", + "@supabase/postgrest-js": "2.118.0", + "@supabase/realtime-js": "2.118.0", + "@supabase/storage-js": "2.118.0" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz", + "integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/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/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.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/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/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/node": { + "version": "24.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "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/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/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/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz", + "integrity": "sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/type-utils": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.71.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", + "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.2.tgz", + "integrity": "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.2.tgz", + "integrity": "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.71.2", + "@typescript-eslint/types": "^8.71.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.2.tgz", + "integrity": "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.2.tgz", + "integrity": "sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.2.tgz", + "integrity": "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.2.tgz", + "integrity": "sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.2.tgz", + "integrity": "sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.71.2", + "@typescript-eslint/tsconfig-utils": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.2.tgz", + "integrity": "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.2.tgz", + "integrity": "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/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/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/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==", + "dev": 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/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.5.2.tgz", + "integrity": "sha512-ES8WeJ2fNWPEDunyAtUfJ12nHyaWUloCHdkfK2oW5uSoQSUmjNKdWiGaXJITJwvFEWoD/evHEg/QoCXAvLaQqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.5.2", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^8.0.0", + "babel-preset-jest": "30.5.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-8.0.0.tgz", + "integrity": "sha512-18wCskrN3DgbuBmp1gr7LBGT8xdz5xhQQqFvFhVxbkl8VBCrMKQ2YtqBWtUal1Zrc1HTuX0011+Brjw78TCFkg==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.5.0.tgz", + "integrity": "sha512-gtGo1B+u14jrZQv6TdSWIWkTqclboo7Qn+dFAGUOIuXLFuJKAdg3U5MIWzZnW4vfUb4dX9skmAMiby86e/SF4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.5.0.tgz", + "integrity": "sha512-ZGPn5ClP4lBDpuOK8W1yQIOy359HmbnZv3suucAlIe+SEE9yDsxV4S2PjSbH2Vc97U+WmzYD7vw3kr8NsQ/i6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.5.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1 || ^8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.29.tgz", + "integrity": "sha512-YeesxhUmR4h5y5V8KF8iBCqJb8YFrBJ++XBTeGcWIaQOYXGN19JM0FoiOjAhwuDoln8owMgXcWBmgHtYSkky2w==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "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/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.2.tgz", + "integrity": "sha512-V2o8YS2EhrKMO9AumnkK0ynynfFlsajpYupwq7S46H5mhcanwxDkNTw3K67wPIcKTH1ffkA4vhMOtAc7gRmPZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001816", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001816.tgz", + "integrity": "sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.3.0.tgz", + "integrity": "sha512-lmNyBzi6iYiqyrExTctfmjd8tInx+dHu9PBomTfH5RXS53c27JMFJgzNv5TyAnm3PgGf+MLy0+Ws41/HlVxdKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz", + "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-5.0.0.tgz", + "integrity": "sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/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-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/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.8.tgz", + "integrity": "sha512-2KDeoFoK/zRtcnbunCDlLG5VWVMcyqeQc6iK65o231nfNAcuGrKoqUQfJFUtBI+QlShk5HKIHxwebBtUl3rEfA==", + "dev": true, + "bin": { + "dotenv": "dist/index.cjs" + }, + "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/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/electron-to-chromium": { + "version": "1.5.459", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.459.tgz", + "integrity": "sha512-4SC9HHSFjO/liAhifpdOL5hG9b4BP7/5G3y3UPZVO8trlAXr1CaUEEJL0gWK3kOcYcZrOA5rkwG45Gra/lTIgg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.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==", + "dev": true, + "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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.7.tgz", + "integrity": "sha512-WjHyn3GrFd+ZR0rzsLiM4zJZ35PZMOE0SUIxzGmnWNG0126yNfJKJzObmmkYDwHJfGlkz3PSlzyG9ez/LJntwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.12.3" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.5.2.tgz", + "integrity": "sha512-0LNuMvs8/5mRYhnCR4k+lGV7fqPMs6Bnksda4S3zsCUmdxBpAn4zU4NNzXdq1pMKe/H4W5t/zVIDSJ/H3e0vDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.5.2", + "@jest/get-type": "30.5.0", + "jest-matcher-utils": "30.5.2", + "jest-message-util": "30.5.1", + "jest-mock": "30.5.2", + "jest-util": "30.5.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fb-watchman": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.4.tgz", + "integrity": "sha512-tGQtaoYisPDriJBN0mT5yZSyNETnJGA1gTYxaVRAE5KUO3LI9kPUc9WIu8JXcxaIci+faIKlzXqXyPgrmubtBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bser": "2.1.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "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-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^4.0.1", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz", + "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.10", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.10.tgz", + "integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.8", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "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/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "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/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/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/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-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-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.5.2.tgz", + "integrity": "sha512-3gnpdBIza8ijCl3iMV9ChE3hSt1+46865qqod863gQtJr1DixOkAU5DoGbi3u+XlRcw++BhZQMu/y4xBdHBvSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.5.2", + "@jest/types": "30.5.1", + "import-local": "^3.2.0", + "jest-cli": "30.5.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.5.1.tgz", + "integrity": "sha512-0+bvMM/ENhDI29Z8q1r4HxiDIi4G5tnBmSw4esfPQoj9q8Nik7KIyuxHkTVnnniJQf05SxpGaKDQ+4h28ynGPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.5.1", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.5.2.tgz", + "integrity": "sha512-vffFgjs5JSJ9q4ds1vNW3klKYPJfYfIY61LT/zaZgvTeASJOWZUt8LBfHgIcV4rwxlPBLl/ePBGccHEclS4PlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.5.2", + "@jest/expect": "30.5.2", + "@jest/test-result": "30.5.2", + "@jest/types": "30.5.1", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.5.2", + "jest-matcher-utils": "30.5.2", + "jest-message-util": "30.5.1", + "jest-runtime": "30.5.2", + "jest-snapshot": "30.5.2", + "jest-util": "30.5.1", + "p-limit": "^3.1.0", + "pretty-format": "30.5.1", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.5.2.tgz", + "integrity": "sha512-YYYxUUVjFgPvgEleKNKMaYGIFgR3l3832Cl0kRL/oCiDZ03v7wImUtquHl2OOBCCNgprjZgBN5bczS/JklPBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.5.2", + "@jest/test-result": "30.5.2", + "@jest/types": "30.5.1", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.5.2", + "jest-util": "30.5.1", + "jest-validate": "30.5.1", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.5.2.tgz", + "integrity": "sha512-U6221OZekabePvGcGPf4raIBKMvYQWFHvhobDJRojOYaFMj2HaGytAdknjDhYc6JTz2fdHW9+6k0wWUDaNeOFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.5.0", + "@jest/pattern": "30.5.0", + "@jest/test-sequencer": "30.5.2", + "@jest/types": "30.5.1", + "babel-jest": "30.5.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^13.0.6", + "graceful-fs": "^4.2.11", + "jest-circus": "30.5.2", + "jest-docblock": "30.5.0", + "jest-environment-node": "30.5.2", + "jest-regex-util": "30.5.0", + "jest-resolve": "30.5.1", + "jest-runner": "30.5.2", + "jest-util": "30.5.1", + "jest-validate": "30.5.1", + "parse-json": "^5.2.0", + "pretty-format": "30.5.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.5.2.tgz", + "integrity": "sha512-rBtHnI6BWTiWj3lM7fOLVfChx7R9B0u9VV3PRHawKs79x6ZjW1QoSrKenaigNGWlvhtoi2BqvDbABDie+Os3xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.5.0", + "@jest/get-type": "30.5.0", + "chalk": "^4.1.2", + "pretty-format": "30.5.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.5.0.tgz", + "integrity": "sha512-NwDqcxtoZi33RhuW+zJS/RVA3rmheQ8BnwpYZuc/Eruaz6seQb7+aoCeDZu/3X7W2XmD8DbSo9Pn72DbKsBFYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.5.2.tgz", + "integrity": "sha512-GRrW+7fmmgmkvyfEBqc1QFdWnF1Ex5yG6Y6AWL4TTxnkWlKIT98dsKh3P3UuWm6v4XsQwVE6DEzXIPMzWJ3ZbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.5.0", + "@jest/types": "30.5.1", + "chalk": "^4.1.2", + "jest-regex-util": "30.5.0", + "jest-util": "30.5.1", + "pretty-format": "30.5.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.5.2.tgz", + "integrity": "sha512-Ciz4KgTGIbojxSKpuFImgWJj5EyqfE8xMq9NxWmOXnQywHHS03H9Mv+vaCB29J5pr3JBrY27WbRV18rBdjh8Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.5.2", + "@jest/fake-timers": "30.5.2", + "@jest/types": "30.5.1", + "@types/node": "*", + "jest-mock": "30.5.2", + "jest-util": "30.5.1", + "jest-validate": "30.5.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.5.1.tgz", + "integrity": "sha512-VIFgt67jW480YDxKfEv9IYQKrFpYt7bOCMn3VsnjK7AK9qo7p6acpnA1DHwsVOULE3dTaYew/6JaTD/d0VDHoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.5.1", + "@parcel/watcher": "^2.6.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "fdir": "^6.5.0", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.5.0", + "jest-util": "30.5.1", + "jest-worker": "30.5.1", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.5.1.tgz", + "integrity": "sha512-gt4GT2aWgEoCNTcBe4rqS74xTIUJxi+UD9SNSu9aOk5LmTEd6fS5KSTmAKAfitCCktQHNN+upUuL6EkBfFbMDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.5.0", + "pretty-format": "30.5.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.5.2.tgz", + "integrity": "sha512-lFkB365PTIHOhPzwrb9T6gvlDnPpOZ7/c3ewOKKB7bzztqjVrtlyRL2MkyfJXpyY2u2SKw001JKrTx14fBUFcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.5.0", + "chalk": "^4.1.2", + "jest-diff": "30.5.2", + "pretty-format": "30.5.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.5.1.tgz", + "integrity": "sha512-UdQlLdd9wL/Ys7xRErckqwD6wPlSZYueosSWuHc1r2ztGLwlgPvtSJq2+BPEgaEY13WLvfFbmhTj8pba0Sd1jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.5.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.5.1", + "picomatch": "^4.0.3", + "pretty-format": "30.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.5.2.tgz", + "integrity": "sha512-KTHb37Fhj8xY8qPFvTFaFJHcdsumQp+ExsUlgoX232Agmc706gagBs53+CYvulV7MVQo9AgXOUxsqzzVBoJlrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.5.2", + "@jest/types": "30.5.1", + "@types/node": "*", + "jest-util": "30.5.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-regex-util": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.5.0.tgz", + "integrity": "sha512-Mg0WK7A6xRHLSA1udJ8y9f3lM0uUhFTBnLKzwPmqB9AylvpleJ6BLemR8K9dK27DY+cesDryoA7yLZCAHsPG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.5.1.tgz", + "integrity": "sha512-wprhLejRtwN6h8ZgaqC0eYjGJ1uMdGPT/+b3eFncbG4NWuuP9QL+vWwRinu9waw7hkOLcpMJGVJAMgBwsPssMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.5.1", + "jest-util": "30.5.1", + "jest-validate": "30.5.1", + "slash": "^3.0.0", + "unrs-resolver": "^1.12.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.5.2.tgz", + "integrity": "sha512-GMI0DP5enX9Z2qW2zFy1bOkrCfWAOPRJQ7qHt0pdfrxBPsgWxglMPrRSK2TX/wRWYxcPRSLFg3Z88qPOzHx7FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.5.0", + "jest-snapshot": "30.5.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.5.2.tgz", + "integrity": "sha512-/B6px7hiiNhVSRwuv9AGn0nawYvwsHeQZ3ItTd5Gp2diFd0EOKaWVeWqcbw3L88vx1GfpCKjQkxWItZCTl11Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.5.2", + "@jest/environment": "30.5.2", + "@jest/source-map": "30.5.2", + "@jest/test-result": "30.5.2", + "@jest/transform": "30.5.2", + "@jest/types": "30.5.1", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.5.0", + "jest-environment-node": "30.5.2", + "jest-haste-map": "30.5.1", + "jest-leak-detector": "30.5.1", + "jest-message-util": "30.5.1", + "jest-resolve": "30.5.1", + "jest-runtime": "30.5.2", + "jest-util": "30.5.1", + "jest-watcher": "30.5.2", + "jest-worker": "30.5.1", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.5.2.tgz", + "integrity": "sha512-0paUAHBlheai10A3XSy9Xh+YBG0IROy+/qzcDPzQ/nbbXrznQzeWy5qniICWK6d3W3scaRQCVAbXdcK0ibBq4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.5.2", + "@jest/fake-timers": "30.5.2", + "@jest/globals": "30.5.2", + "@jest/source-map": "30.5.2", + "@jest/test-result": "30.5.2", + "@jest/transform": "30.5.2", + "@jest/types": "30.5.1", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.2.0", + "collect-v8-coverage": "^1.0.2", + "es-module-lexer": "^2.1.0", + "glob": "^13.0.6", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.5.1", + "jest-message-util": "30.5.1", + "jest-mock": "30.5.2", + "jest-regex-util": "30.5.0", + "jest-resolve": "30.5.1", + "jest-snapshot": "30.5.2", + "jest-util": "30.5.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.5.2.tgz", + "integrity": "sha512-jKIdes25AnAH73dOCi2qE6J6vHJ6L2vyBS11M89kT50DVJcLTf2AOjvcc+2vVaN8lUbY5v2R83aqXo3Ro9fqYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.5.2", + "@jest/get-type": "30.5.0", + "@jest/snapshot-utils": "30.5.1", + "@jest/transform": "30.5.2", + "@jest/types": "30.5.1", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.5.2", + "graceful-fs": "^4.2.11", + "jest-diff": "30.5.2", + "jest-matcher-utils": "30.5.2", + "jest-message-util": "30.5.1", + "jest-util": "30.5.1", + "pretty-format": "30.5.1", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/jest-util": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.5.1.tgz", + "integrity": "sha512-yKuxmNy2rSbTXw+3SIPanJo+nV4/BS1p26v44IYBFMsswSQySfMMcPHErnOncda7i9HEz0q605rIhSTBVgrZTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.5.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.5.1.tgz", + "integrity": "sha512-i/buJ56wTpxihE93hQYNMfdOThS87+HGvLZzZJmU4xggPcdTYQq051iwALLCHp3q+SkCGH+EFejQZz5EbBSkkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.5.0", + "@jest/types": "30.5.1", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.5.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.5.2.tgz", + "integrity": "sha512-rqRgg4R11GA9m1UsEZwQeixHsgZReCn6TOHAHtIH33yXrIvoX0OGPA20Yzzjkz7X40fZvcRSxEpQppYPcWzmcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.5.2", + "@jest/types": "30.5.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.5.1", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.5.1.tgz", + "integrity": "sha512-Cbxh5v7AoLuFRmFJSM4/aHdQ68rjXvUWr716EE0Dh3I7T+T/3FgFKhOERGXHcU2Meftq9+9zxPM3TSNyI9D+HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.5.1", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.13.15", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.15.tgz", + "integrity": "sha512-AHdhEQeDFEJ4GKDAQ9AwxChy+v2kUILUB3TGQRpkXMkWePjXTU/WQEzWVQoIx/Gz8fE73Wr5SEuZCy2vk8jrMQ==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-esm": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", + "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/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/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "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/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "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/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.13.1.tgz", + "integrity": "sha512-1mhAfvachWYYOm8tpmany5NLuvP+F2wxZWBSX8DC0uFS2wJpM9q3rWgN9wBooAH9d5V12wX0BjyJe4fMjHMarQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "jest-worker": "^27.4.5", + "schema-utils": "^4.6.0", + "terser": "^5.51.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@napi-rs/image": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "imagemin": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "sharp": { + "optional": true + }, + "svgo": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/minimizer-webpack-plugin/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==", + "dev": true, + "license": "MIT", + "peer": true, + "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/minimizer-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/minimizer-webpack-plugin/node_modules/schema-utils": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.8.1.tgz", + "integrity": "sha512-x39pYlJPyH7Uge5QO64B9XGP9zLDc9WNmiQZc+CE3IzZPvMu3lHOuAMGpYNoPixEL7MSWlGhYTg5UlLSL8G6iw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0" + }, + "bin": { + "schema-utils": "bin/schema-utils.js" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.112.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.4.0.tgz", + "integrity": "sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/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/multer/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/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "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/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.59", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.59.tgz", + "integrity": "sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "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/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/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/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": 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.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz", + "integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==", + "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/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/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/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/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.6.2.tgz", + "integrity": "sha512-BkdixFFUt7r7LsX6aGNPVyYMLjW+M1USLlKPNbZsrZEmLFh5tGqhdfckKo6xgsZIJqdDD0CZGkeCJJwNWQIKQA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/superagent": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.4.1.tgz", + "integrity": "sha512-PVMkMrhKrSTtFhUU8jiWuGh/gyRMKiyCTCmm0+qVzRNxlfntmRKJkDhlEXN62x3HIb33sQ4kmL8lcSdccWAchQ==", + "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.3.1", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.3.1.tgz", + "integrity": "sha512-vNaNCFoCR/edDbErQZp5KLNT8R4dSPHX7rlDLoE8hWLAm/878j6ShMbe8HK/E6RwwuhlSXVelXAIg9AxR5WWPw==", + "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/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.12.3.tgz", + "integrity": "sha512-U+/AXT6LWsxnj2vz9HeUuPWBNgdWxjyIEjEPdbJARC9txmgPzo2VEdqnFo4hHaNyMPtycyVJrSaQYeQv/PoyyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dual-bundle/import-meta-resolve": "^4.2.1", + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/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==", + "dev": 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/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.8.1.tgz", + "integrity": "sha512-x39pYlJPyH7Uge5QO64B9XGP9zLDc9WNmiQZc+CE3IzZPvMu3lHOuAMGpYNoPixEL7MSWlGhYTg5UlLSL8G6iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0" + }, + "bin": { + "schema-utils": "bin/schema-utils.js" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.112.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/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/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.15", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.15.tgz", + "integrity": "sha512-s2qmEUYFRqbjz1wv0EmccNPVOb45HOFpFuhzQJLKShYVisuTbCFPIIAIV2EuK5rIxb4BejLDbH7dnBZfYjybUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.10", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.8.5", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <9", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-loader": { + "version": "9.6.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.6.2.tgz", + "integrity": "sha512-R4iuczmtgxvtuI556s+hTZ6/7Ee03VCAk/l/M8LY1OAsUgB7YydsCxkgq9D9pKRaD7GJqUi2u8fp9zZP/ufjKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "picomatch": "^4.0.0", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "loader-utils": "*", + "typescript": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "loader-utils": { + "optional": true + } + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "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": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.2.tgz", + "integrity": "sha512-Rz4effbMy2g9wnvOJZG6SbVavX313Lap1vNEbk1RUochzickvzP8P4HB+bkNg5e50eVz1hQGjh9K1omI1xB4Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.71.2", + "@typescript-eslint/parser": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.6.0.tgz", + "integrity": "sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/watchpack": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.6.3.tgz", + "integrity": "sha512-s3NZy0QpTY9qGqzc0wtz4qI28z3+5qKbbAec4MV4G9N3CfgZ9wgvQRr0cl2jh/tqkWG1NWHkmVMHZpCamev5Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webpack": { + "version": "5.112.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.112.0.tgz", + "integrity": "sha512-NIBpsRuCll036haDDt/CfcTDfsWNR9Z782nMRMI9D377BsGcYzZDTNU5TyzhCJzPIcihAxMBaMAhq2WSaDrH8Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.26.0", + "es-module-lexer": "^2.1.0", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.13.0", + "schema-utils": "^4.5.0", + "tapable": "^2.3.0", + "watchpack": "^2.6.1", + "webpack-sources": "^3.6.0" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.6.0.tgz", + "integrity": "sha512-EIMmPVvNI0CYXyRPp34F2Qk7W37/BZVZIofS6LAUnrNjCVahS+hO4mxwFJDjQjODTGZDtVzLYoN2+1dYkhk9qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/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==", + "dev": true, + "license": "MIT", + "peer": true, + "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/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.8.1.tgz", + "integrity": "sha512-x39pYlJPyH7Uge5QO64B9XGP9zLDc9WNmiQZc+CE3IzZPvMu3lHOuAMGpYNoPixEL7MSWlGhYTg5UlLSL8G6iw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0" + }, + "bin": { + "schema-utils": "bin/schema-utils.js" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.112.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..24a63fc --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,75 @@ +{ + "name": "api", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json" + }, + "dependencies": { + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "@nestjs/platform-express": "^11.0.1", + "@supabase/supabase-js": "^2.118.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.18.0", + "@nestjs/cli": "^11.0.0", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.1", + "@types/express": "^5.0.0", + "@types/jest": "^30.0.0", + "@types/node": "^24.0.0", + "@types/supertest": "^7.2.1", + "dotenv": "^18.0.8", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2", + "globals": "^17.0.0", + "jest": "^30.0.0", + "prettier": "^3.4.2", + "source-map-support": "^0.5.21", + "supertest": "^7.3.1", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/apps/api/src/account/account.controller.ts b/apps/api/src/account/account.controller.ts new file mode 100644 index 0000000..623d54d --- /dev/null +++ b/apps/api/src/account/account.controller.ts @@ -0,0 +1,25 @@ +import { Body, Controller, Get, Patch, Req, UseGuards } from "@nestjs/common"; +import type { AuthedRequest } from "../auth/supabase-auth.guard"; +import { SupabaseAuthGuard } from "../auth/supabase-auth.guard"; +import { AccountService } from "./account.service"; +import { UpdatePreferencesDto, UpdateProfileDto } from "./dto"; +@Controller("account") +@UseGuards(SupabaseAuthGuard) +export class AccountController { + constructor(private readonly account: AccountService) {} + + @Get() + get(@Req() req: AuthedRequest) { + return this.account.get(req.user.id); + } + + @Patch("profile") + updateProfile(@Req() req: AuthedRequest, @Body() dto: UpdateProfileDto) { + return this.account.updateProfile(req.user.id, dto); + } + + @Patch("notification-preferences") + updatePreferences(@Req() req: AuthedRequest, @Body() dto: UpdatePreferencesDto) { + return this.account.updatePreferences(req.user.id, dto); + } +} diff --git a/apps/api/src/account/account.module.ts b/apps/api/src/account/account.module.ts new file mode 100644 index 0000000..bdb4353 --- /dev/null +++ b/apps/api/src/account/account.module.ts @@ -0,0 +1,10 @@ +import { Module } from "@nestjs/common"; +import { SupabaseAuthGuard } from "../auth/supabase-auth.guard"; +import { AccountController } from "./account.controller"; +import { AccountService } from "./account.service"; + +@Module({ + controllers: [AccountController], + providers: [AccountService, SupabaseAuthGuard], +}) +export class AccountModule {} diff --git a/apps/api/src/account/account.service.ts b/apps/api/src/account/account.service.ts new file mode 100644 index 0000000..d21a3b4 --- /dev/null +++ b/apps/api/src/account/account.service.ts @@ -0,0 +1,75 @@ +import { + ConflictException, + Injectable, + InternalServerErrorException, + NotFoundException, +} from "@nestjs/common"; +import { SupabaseService } from "../supabase/supabase.service"; +import { UpdatePreferencesDto, UpdateProfileDto } from "./dto"; + +const toProfile = (r: any) => ({ + username: r.username, + displayName: r.display_name, + phone: r.phone, +}); +const toPrefs = (r: any) => ({ + priceAlerts: r.price_alerts, + accountAlerts: r.account_alerts, +}); + +@Injectable() +export class AccountService { + constructor(private readonly supabase: SupabaseService) {} + + private get db() { + return this.supabase.admin; + } + + async get(userId: string) { + const [profile, prefs] = await Promise.all([ + this.db.from("profiles").select().eq("id", userId).maybeSingle(), + this.db.from("notification_preferences").select().eq("user_id", userId).maybeSingle(), + ]); + if (profile.error || prefs.error) throw new InternalServerErrorException(); + if (!profile.data || !prefs.data) throw new NotFoundException(); + + return { + profile: toProfile(profile.data), + notificationPreferences: toPrefs(prefs.data), + }; + } + + async updateProfile(userId: string, dto: UpdateProfileDto) { + const { data, error } = await this.db + .from("profiles") + .update({ + username: dto.username, + display_name: dto.displayName, + phone: dto.phone, + updated_at: new Date().toISOString(), + }) + .eq("id", userId) + .select() + .single(); + + if (error?.code === "23505") throw new ConflictException("Username is taken."); + if (error || !data) throw new InternalServerErrorException(); + return toProfile(data); + } + + async updatePreferences(userId: string, dto: UpdatePreferencesDto) { + const { data, error } = await this.db + .from("notification_preferences") + .update({ + price_alerts: dto.priceAlerts, + account_alerts: dto.accountAlerts, + updated_at: new Date().toISOString(), + }) + .eq("user_id", userId) + .select() + .single(); + + if (error || !data) throw new InternalServerErrorException(); + return toPrefs(data); + } +} diff --git a/apps/api/src/account/dto.ts b/apps/api/src/account/dto.ts new file mode 100644 index 0000000..9706c84 --- /dev/null +++ b/apps/api/src/account/dto.ts @@ -0,0 +1,21 @@ +import { IsBoolean, IsOptional, IsString, Matches, MaxLength } from "class-validator"; + +export class UpdateProfileDto { + @IsOptional() + @Matches(/^[a-zA-Z0-9_]{3,30}$/, { + message: "username must be 3-30 letters, numbers or underscores", + }) + username?: string; + + @IsOptional() @IsString() @MaxLength(50) + displayName?: string; + + @IsOptional() + @Matches(/^\+?[0-9]{7,15}$/, { message: "invalid phone number" }) + phone?: string; +} + +export class UpdatePreferencesDto { + @IsOptional() @IsBoolean() priceAlerts?: boolean; + @IsOptional() @IsBoolean() accountAlerts?: boolean; +} diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts new file mode 100644 index 0000000..78cdf78 --- /dev/null +++ b/apps/api/src/app.module.ts @@ -0,0 +1,8 @@ +import { Module } from "@nestjs/common"; +import { AccountModule } from "./account/account.module"; +import { SupabaseModule } from "./supabase/supabase.module"; + +@Module({ + imports: [SupabaseModule, AccountModule], +}) +export class AppModule {} \ No newline at end of file diff --git a/apps/api/src/app.setup.ts b/apps/api/src/app.setup.ts new file mode 100644 index 0000000..495d07f --- /dev/null +++ b/apps/api/src/app.setup.ts @@ -0,0 +1,8 @@ +import { INestApplication, ValidationPipe } from "@nestjs/common"; + +export function configureApp(app: INestApplication) { + app.useGlobalPipes( + new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }), + ); + return app; +} diff --git a/apps/api/src/auth/supabase-auth.guard.ts b/apps/api/src/auth/supabase-auth.guard.ts new file mode 100644 index 0000000..b141b15 --- /dev/null +++ b/apps/api/src/auth/supabase-auth.guard.ts @@ -0,0 +1,27 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, +} from "@nestjs/common"; +import { Request } from "express"; +import { SupabaseService } from "../supabase/supabase.service"; + +export type AuthedRequest = Request & { user: { id: string } }; + +@Injectable() +export class SupabaseAuthGuard implements CanActivate { + constructor(private readonly supabase: SupabaseService) {} + + async canActivate(ctx: ExecutionContext): Promise { + const req = ctx.switchToHttp().getRequest(); + const [scheme, token] = (req.headers.authorization ?? "").split(" "); + if (scheme !== "Bearer" || !token) throw new UnauthorizedException(); + + const { data, error } = await this.supabase.admin.auth.getUser(token); + if (error || !data.user) throw new UnauthorizedException(); + + req.user = { id: data.user.id }; + return true; + } +} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts new file mode 100644 index 0000000..9d84dec --- /dev/null +++ b/apps/api/src/main.ts @@ -0,0 +1,10 @@ +import "dotenv/config"; +import { NestFactory } from "@nestjs/core"; +import { AppModule } from "./app.module"; +import { configureApp } from "./app.setup"; + +async function bootstrap() { + const app = configureApp(await NestFactory.create(AppModule)); + await app.listen(process.env.PORT ?? 3000, "0.0.0.0"); +} +bootstrap(); \ No newline at end of file diff --git a/apps/api/src/supabase/supabase.module.ts b/apps/api/src/supabase/supabase.module.ts new file mode 100644 index 0000000..b3fdbea --- /dev/null +++ b/apps/api/src/supabase/supabase.module.ts @@ -0,0 +1,6 @@ +import { Global, Module } from "@nestjs/common"; +import { SupabaseService } from "./supabase.service"; + +@Global() +@Module({ providers: [SupabaseService], exports: [SupabaseService] }) +export class SupabaseModule {} \ No newline at end of file diff --git a/apps/api/src/supabase/supabase.service.ts b/apps/api/src/supabase/supabase.service.ts new file mode 100644 index 0000000..edf25e6 --- /dev/null +++ b/apps/api/src/supabase/supabase.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from "@nestjs/common"; +import { createClient, SupabaseClient } from "@supabase/supabase-js"; + +function requireEnv(name: string): string { + const value = process.env[name]; + if (!value) throw new Error(`Missing environment variable: ${name}`); + return value; +} + +@Injectable() +export class SupabaseService { + // Service-role client: bypasses RLS. Server-side only. + readonly admin: SupabaseClient = createClient( + requireEnv("SUPABASE_URL"), + requireEnv("SUPABASE_SERVICE_ROLE_KEY"), + { auth: { persistSession: false, autoRefreshToken: false } }, + ); +} \ No newline at end of file diff --git a/apps/api/test/account.e2e-spec.ts b/apps/api/test/account.e2e-spec.ts new file mode 100644 index 0000000..8143999 --- /dev/null +++ b/apps/api/test/account.e2e-spec.ts @@ -0,0 +1,156 @@ +import { INestApplication } from "@nestjs/common"; +import { Test } from "@nestjs/testing"; +import { createClient } from "@supabase/supabase-js"; +import request from "supertest"; +import { AppModule } from "../src/app.module"; +import { configureApp } from "../src/app.setup"; + +jest.setTimeout(120_000); + +const URL = process.env.SUPABASE_URL ?? "http://127.0.0.1:54321"; +const ANON = process.env.SUPABASE_ANON_KEY!; +const SERVICE = process.env.SUPABASE_SERVICE_ROLE_KEY!; + +const admin = createClient(URL, SERVICE, { auth: { persistSession: false } }); +const anon = createClient(URL, ANON, { auth: { persistSession: false } }); + +async function createUser(label: string) { + const suffix = `${Date.now()}${Math.floor(Math.random() * 1000)}`; + const email = `${label}${suffix}@example.com`; + const username = `${label}_${suffix}`.slice(0, 30); + + const created = await admin.auth.admin.createUser({ + email, + password: "password123", + email_confirm: true, + user_metadata: { username }, + }); + if (created.error) throw created.error; + + const { data, error } = await anon.auth.signInWithPassword({ + email, + password: "password123", + }); + if (error || !data.session) throw error; + + return { id: created.data.user.id, username, token: data.session.access_token }; +} + +async function buildApp() { + const moduleRef = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + const app = configureApp(moduleRef.createNestApplication()); + await app.init(); + return app; +} + +describe("account API", () => { + let app: INestApplication; + let userA: Awaited>; + let userB: Awaited>; + +beforeAll(async () => { + app = await buildApp(); + userA = await createUser("usera"); + userB = await createUser("userb"); +}, 120_000); + +afterAll(async () => { + await app?.close(); + if (userA) await admin.auth.admin.deleteUser(userA.id); + if (userB) await admin.auth.admin.deleteUser(userB.id); +}, 60_000); + + const auth = (token: string) => ({ Authorization: `Bearer ${token}` }); + + it("rejects a request with no token", async () => { + await request(app.getHttpServer()).get("/account").expect(401); + }); + + it("rejects an invalid token", async () => { + await request(app.getHttpServer()) + .get("/account") + .set(auth("garbage")) + .expect(401); + }); + + it("returns only the caller's own account", async () => { + await request(app.getHttpServer()) + .patch("/account/profile") + .set(auth(userA.token)) + .send({ displayName: "Alice" }) + .expect(200); + await request(app.getHttpServer()) + .patch("/account/profile") + .set(auth(userB.token)) + .send({ displayName: "Bob" }) + .expect(200); + + const a = await request(app.getHttpServer()) + .get("/account") + .set(auth(userA.token)) + .expect(200); + const b = await request(app.getHttpServer()) + .get("/account") + .set(auth(userB.token)) + .expect(200); + + expect(a.body.profile.displayName).toBe("Alice"); + expect(a.body.profile.username).toBe(userA.username); + expect(b.body.profile.displayName).toBe("Bob"); + expect(b.body.profile.username).toBe(userB.username); + }); + + it("cannot target another user by sending an id", async () => { + await request(app.getHttpServer()) + .patch("/account/profile") + .set(auth(userA.token)) + .send({ id: userB.id, displayName: "Hacked" }) + .expect(400); + + const b = await request(app.getHttpServer()) + .get("/account") + .set(auth(userB.token)) + .expect(200); + expect(b.body.profile.displayName).toBe("Bob"); + }); + + it("returns 409 for a duplicate username, ignoring case", async () => { + await request(app.getHttpServer()) + .patch("/account/profile") + .set(auth(userA.token)) + .send({ username: userB.username.toUpperCase() }) + .expect(409); + }); + + it("returns 400 for invalid notification fields", async () => { + await request(app.getHttpServer()) + .patch("/account/notification-preferences") + .set(auth(userA.token)) + .send({ priceAlerts: "yes" }) + .expect(400); + await request(app.getHttpServer()) + .patch("/account/notification-preferences") + .set(auth(userA.token)) + .send({ foo: true }) + .expect(400); + }); + + it("persists updates after the app restarts", async () => { + await request(app.getHttpServer()) + .patch("/account/notification-preferences") + .set(auth(userA.token)) + .send({ priceAlerts: false }) + .expect(200); + + await app.close(); + app = await buildApp(); + + const res = await request(app.getHttpServer()) + .get("/account") + .set(auth(userA.token)) + .expect(200); + expect(res.body.notificationPreferences.priceAlerts).toBe(false); + }); +}); \ No newline at end of file diff --git a/apps/api/test/jest-e2e.json b/apps/api/test/jest-e2e.json new file mode 100644 index 0000000..f818ac2 --- /dev/null +++ b/apps/api/test/jest-e2e.json @@ -0,0 +1,10 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "setupFiles": ["/setup-env.ts"] +} diff --git a/apps/api/test/setup-env.ts b/apps/api/test/setup-env.ts new file mode 100644 index 0000000..fd3e093 --- /dev/null +++ b/apps/api/test/setup-env.ts @@ -0,0 +1,2 @@ +import { config } from "dotenv"; +config({ path: ".env" }); \ No newline at end of file diff --git a/apps/api/tsconfig.build.json b/apps/api/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/apps/api/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..aba29b0 --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "resolvePackageJsonExports": true, + "esModuleInterop": true, + "isolatedModules": true, + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2023", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": false, + "strictBindCallApply": false, + "noFallthroughCasesInSwitch": false + } +} diff --git a/supabase/.gitignore b/supabase/.gitignore new file mode 100644 index 0000000..ad9264f --- /dev/null +++ b/supabase/.gitignore @@ -0,0 +1,8 @@ +# Supabase +.branches +.temp + +# dotenvx +.env.keys +.env.local +.env.*.local diff --git a/supabase/config.toml b/supabase/config.toml new file mode 100644 index 0000000..0e7be29 --- /dev/null +++ b/supabase/config.toml @@ -0,0 +1,415 @@ +# For detailed configuration reference documentation, visit: +# https://supabase.com/docs/guides/local-development/cli/config +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "quickcop" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. `public` and `graphql_public` schemas are included by default. +schemas = ["public", "graphql_public"] +# Extra schemas to add to the search_path of every request. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 +# Controls whether new tables, views, sequences and functions created in the `public` schema by +# `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) +# without explicit GRANTs, matching the cloud default. Set to `false` to require explicit GRANTs +# instead. Left unset, a fresh project falls back to `true`. +# auto_expose_new_tables = true + +[api.tls] +# Enable HTTPS endpoints locally using a self-signed certificate. +enabled = false +# Paths to self-signed certificate pair. +# cert_path = "../certs/my-cert.pem" +# key_path = "../certs/my-key.pem" + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# Maximum amount of time to wait for health check when starting the local database. +health_timeout = "2m" +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 17 +# OrioleDB version to use as the Postgres storage engine. Leave empty to use the default engine. +orioledb_version = "" + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +# [db.vault] +# secret_key = "env(SECRET_VALUE)" + +[db.migrations] +# If disabled, migrations will be skipped during a db push or reset. +enabled = true +# Specifies an ordered list of schema files, directories, or glob patterns that describe your database. +# Supports paths relative to supabase directory: "./schemas/*.sql", "./database". +schema_paths = [] + +[db.seed] +# If enabled, seeds the database after migrations during a db reset. +enabled = true +# Specifies an ordered list of seed files to load during db reset. +# Supports glob patterns relative to supabase directory: "./seeds/*.sql" +sql_paths = ["./seed.sql"] + +[db.network_restrictions] +# Enable management of network restrictions. +enabled = false +# List of IPv4 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv4 connections. Set empty array to block all IPs. +allowed_cidrs = ["0.0.0.0/0"] +# List of IPv6 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv6 connections. Set empty array to block all IPs. +allowed_cidrs_v6 = ["::/0"] + +# Uncomment to reject non-secure connections to the database. +# [db.ssl_enforcement] +# enabled = true + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv4) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[local_smtp] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 +# admin_email = "admin@email.com" +# sender_name = "Admin" + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +# Uncomment to configure local storage buckets +# [storage.buckets.images] +# public = false +# file_size_limit = "50MiB" +# allowed_mime_types = ["image/png", "image/jpeg"] +# objects_path = "./images" + +# Allow connections via S3 compatible clients +[storage.s3_protocol] +enabled = true + +# Image transformation API is available to Supabase Pro plan. +# [storage.image_transformation] +# enabled = true + +# Store analytical data in S3 for running ETL jobs over Iceberg Catalog +# This feature is only available on the hosted platform. +[storage.analytics] +enabled = false +max_namespaces = 5 +max_tables = 10 +max_catalogs = 2 + +# Analytics Buckets is available to Supabase Pro plan. +# [storage.analytics.buckets.my-warehouse] + +# Store vector embeddings in S3 for large and durable datasets +[storage.vector] +enabled = true +max_buckets = 10 +max_indexes = 5 + +# Vector Buckets is available to Supabase Pro plan. +# [storage.vector.buckets.documents-openai] + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# The public URL that Auth serves on. Defaults to the API external URL with `/auth/v1` appended. +# external_url = "" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# JWT issuer URL. If not set, defaults to auth.external_url. +# jwt_issuer = "" +# Path to JWT signing key. DO NOT commit your signing keys file to git. +# signing_keys_path = "./signing_keys.json" +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow anonymous sign-ins to your project. +enable_anonymous_sign_ins = false +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false +# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. +minimum_password_length = 6 +# Passwords that do not meet the following requirements will be rejected as weak. Supported values +# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` +password_requirements = "" + +# Configure passkey sign-ins. +# [auth.passkey] +# enabled = false + +# Configure WebAuthn relying party settings (required when passkey is enabled). +# [auth.webauthn] +# rp_display_name = "Supabase" +# rp_id = "localhost" +# rp_origins = ["http://127.0.0.1:3000"] + +[auth.rate_limit] +# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. +email_sent = 2 +# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. +sms_sent = 30 +# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. +anonymous_users = 30 +# Number of sessions that can be refreshed in a 5 minute interval per IP address. +token_refresh = 150 +# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). +sign_in_sign_ups = 30 +# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. +token_verifications = 30 +# Number of Web3 logins that can be made in a 5 minute interval per IP address. +web3 = 30 + +# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. +# [auth.captcha] +# enabled = true +# provider = "hcaptcha" +# secret = "" + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false +# If enabled, users will need to reauthenticate or have logged in recently to change their password. +secure_password_change = false +# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +max_frequency = "1s" +# Number of characters used in the email OTP. +otp_length = 6 +# Number of seconds before the email OTP expires (defaults to 1 hour). +otp_expiry = 3600 + +# Use a production-ready SMTP server +# [auth.email.smtp] +# enabled = true +# host = "smtp.sendgrid.net" +# port = 587 +# user = "apikey" +# pass = "env(SENDGRID_API_KEY)" +# admin_email = "admin@email.com" +# sender_name = "Admin" + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +# Uncomment to customize notification email template +# [auth.email.notification.password_changed] +# enabled = true +# subject = "Your password has been changed" +# content_path = "./supabase/templates/password_changed_notification.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = false +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ .Code }}" +# Controls the minimum amount of time that must pass before sending another sms otp. +max_frequency = "5s" + +# Use pre-defined map of phone number to OTP for testing. +# [auth.sms.test_otp] +# 4152127777 = "123456" + +# Configure logged in session timeouts. +# [auth.sessions] +# Force log out after the specified duration. +# timebox = "24h" +# Force log out if the user has been inactive longer than the specified duration. +# inactivity_timeout = "8h" + +# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. +# [auth.hook.before_user_created] +# enabled = true +# uri = "pg-functions://postgres/auth/before-user-created-hook" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +# [auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Multi-factor-authentication is available to Supabase Pro plan. +[auth.mfa] +# Control how many MFA factors can be enrolled at once per user. +max_enrolled_factors = 10 + +# Control MFA via App Authenticator (TOTP) +[auth.mfa.totp] +enroll_enabled = false +verify_enabled = false + +# Configure MFA via Phone Messaging +[auth.mfa.phone] +enroll_enabled = false +verify_enabled = false +otp_length = 6 +template = "Your code is {{ .Code }}" +max_frequency = "5s" + +# Configure MFA via WebAuthn +# [auth.mfa.web_authn] +# enroll_enabled = true +# verify_enabled = true + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `x`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth callback URL derived from auth.external_url. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" +# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. +skip_nonce_check = false +# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address. +email_optional = false + +# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. +# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. +[auth.web3.solana] +enabled = false + +# Use Firebase Auth as a third-party provider alongside Supabase Auth. +[auth.third_party.firebase] +enabled = false +# project_id = "my-firebase-project" + +# Use Auth0 as a third-party provider alongside Supabase Auth. +[auth.third_party.auth0] +enabled = false +# tenant = "my-auth0-tenant" +# tenant_region = "us" + +# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. +[auth.third_party.aws_cognito] +enabled = false +# user_pool_id = "my-user-pool-id" +# user_pool_region = "us-east-1" + +# Use Clerk as a third-party provider alongside Supabase Auth. +[auth.third_party.clerk] +enabled = false +# Obtain from https://clerk.com/setup/supabase +# domain = "example.clerk.accounts.dev" + +# OAuth server configuration +[auth.oauth_server] +# Enable OAuth server functionality +enabled = false +# Path for OAuth consent flow UI +authorization_url_path = "/oauth/consent" +# Allow dynamic client registration +allow_dynamic_registration = false + +[edge_runtime] +enabled = true +# Supported request policies: `oneshot`, `per_worker`. +# `per_worker` (default) — enables hot reload during local development. +# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks). +policy = "per_worker" +# Port to attach the Chrome inspector for debugging edge functions. +inspector_port = 8083 +# The Deno major version to use. +deno_version = 2 + +# [edge_runtime.secrets] +# secret_key = "env(SECRET_VALUE)" + +[analytics] +enabled = true +port = 54327 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" + +# pg-delta is the schema diff engine for db diff / db pull / db remote commit. +# Set enabled = false to fall back to the legacy migra engine. +[experimental.pgdelta] +enabled = true +# Directory under `supabase/` where declarative files are written. +# declarative_schema_path = "./schemas" +# JSON string passed through to pg-delta SQL formatting. When omitted, SQL is +# formatted with uppercase keywords, indent 2, max width 180, trailing commas, +# and column/key alignment. Set to "null" to emit raw, unformatted SQL. +# format_options = "{\"keywordCase\":\"upper\",\"indent\":2,\"maxWidth\":180,\"commaStyle\":\"trailing\"}" diff --git a/supabase/migrations/20261019073253_create_account_tables.sql b/supabase/migrations/20261019073253_create_account_tables.sql new file mode 100644 index 0000000..863d236 --- /dev/null +++ b/supabase/migrations/20261019073253_create_account_tables.sql @@ -0,0 +1,51 @@ +create table public.profiles ( + id uuid primary key references auth.users(id) on delete cascade, + username text not null, + display_name text, + phone text, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +create unique index profiles_username_unique +on public.profiles (lower(trim(username))); + +create table public.notification_preferences ( + user_id uuid primary key references auth.users(id) on delete cascade, + price_alerts boolean not null default true, + account_alerts boolean not null default true, + updated_at timestamptz not null default now() +); + +alter table public.profiles enable row level security; +alter table public.notification_preferences enable row level security; + +create policy "users manage their profile" +on public.profiles +for all +using (auth.uid() = id) +with check (auth.uid() = id); + +create policy "users manage their notification preferences" +on public.notification_preferences +for all +using (auth.uid() = user_id) +with check (auth.uid() = user_id); + +create function public.handle_new_user() +returns trigger +language plpgsql +security definer set search_path = '' +as $$ +begin + insert into public.profiles (id, username, phone) + values (new.id, new.raw_user_meta_data->>'username', new.raw_user_meta_data->>'phone'); + + insert into public.notification_preferences (user_id) values (new.id); + return new; +end; +$$; + +create trigger on_auth_user_created +after insert on auth.users +for each row execute function public.handle_new_user(); From 3a7d048c9f2d92588e9c48fda4aa98820edcee2b Mon Sep 17 00:00:00 2001 From: InsertUzername Date: Wed, 21 Oct 2026 00:09:01 -0700 Subject: [PATCH 07/12] fix(db): reconcile hosted account schema --- .../20261020120000_reconcile_profiles_schema.sql | 10 ++++++++++ .../20261020130000_backfill_profiles_and_prefs.sql | 11 +++++++++++ .../20261020150000_align_username_rules.sql | 7 +++++++ 3 files changed, 28 insertions(+) create mode 100644 supabase/migrations/20261020120000_reconcile_profiles_schema.sql create mode 100644 supabase/migrations/20261020130000_backfill_profiles_and_prefs.sql create mode 100644 supabase/migrations/20261020150000_align_username_rules.sql diff --git a/supabase/migrations/20261020120000_reconcile_profiles_schema.sql b/supabase/migrations/20261020120000_reconcile_profiles_schema.sql new file mode 100644 index 0000000..65b946a --- /dev/null +++ b/supabase/migrations/20261020120000_reconcile_profiles_schema.sql @@ -0,0 +1,10 @@ +-- Reconcile hosted public.profiles with repository schema. +-- Additive only: no columns dropped, no data deleted. + +alter table public.profiles + add column if not exists display_name text, + add column if not exists updated_at timestamptz not null default now(); + +-- Legacy column not populated by handle_new_user(); stop it blocking signup. +alter table public.profiles + alter column email drop not null; \ No newline at end of file diff --git a/supabase/migrations/20261020130000_backfill_profiles_and_prefs.sql b/supabase/migrations/20261020130000_backfill_profiles_and_prefs.sql new file mode 100644 index 0000000..b2c8da0 --- /dev/null +++ b/supabase/migrations/20261020130000_backfill_profiles_and_prefs.sql @@ -0,0 +1,11 @@ +-- Backfill legacy profiles created before handle_new_user() was fixed. + +update public.profiles +set display_name = username +where display_name is null; + +insert into public.notification_preferences (user_id) +select p.id +from public.profiles p +left join public.notification_preferences np on np.user_id = p.id +where np.user_id is null; \ No newline at end of file diff --git a/supabase/migrations/20261020150000_align_username_rules.sql b/supabase/migrations/20261020150000_align_username_rules.sql new file mode 100644 index 0000000..ba768ad --- /dev/null +++ b/supabase/migrations/20261020150000_align_username_rules.sql @@ -0,0 +1,7 @@ +-- Username validation lives in handle_new_user(); drop the legacy table check. +alter table public.profiles drop constraint if exists username_format; + +-- Replace legacy lower(username) index with the repo's lower(trim(username)) index. +drop index if exists public.profiles_username_lower_idx; +create unique index if not exists profiles_username_unique + on public.profiles (lower(trim(username))); \ No newline at end of file From 2ed0681791c6704876dc9d15943a24868f139d7a Mon Sep 17 00:00:00 2001 From: InsertUzername Date: Wed, 21 Oct 2026 00:40:05 -0700 Subject: [PATCH 08/12] feat(account): notifications API, mobile account flow, remaining migrations --- .env.example | 9 + .gitattributes | 1 + .gitignore | 5 + .idea/.gitignore | 3 - .idea/caches/deviceStreaming.xml | 2385 ----------------- .idea/deviceManager.xml | 13 - .idea/markdown.xml | 8 - .idea/modules.xml | 8 - .idea/quickcop.iml | 9 - .idea/vcs.xml | 6 - AGENTS.md | 10 +- README.md | 21 +- apps/api/src/account/account.service.ts | 155 +- apps/api/src/account/dto.ts | 47 +- apps/api/src/app.module.ts | 11 +- apps/api/src/auth/supabase-auth.guard.ts | 32 +- apps/api/src/notifications/dto.ts | 6 + .../notifications/notifications.controller.ts | 33 + .../src/notifications/notifications.module.ts | 10 + .../notifications/notifications.service.ts | 69 + apps/api/test/account.e2e-spec.ts | 536 ++-- apps/mobile/.env.example | 1 + apps/mobile/README.md | 3 +- .../__tests__/account-info-container.test.tsx | 174 ++ apps/mobile/__tests__/account-info.test.tsx | 42 +- apps/mobile/__tests__/api-config.test.ts | 41 + apps/mobile/__tests__/auth-flow.test.tsx | 32 +- apps/mobile/__tests__/auth-service.test.ts | 18 + .../__tests__/notification-client.test.ts | 54 + .../notifications-container.test.tsx | 74 + .../__tests__/notifications-screen.test.tsx | 28 +- apps/mobile/app/(protected)/account.tsx | 41 +- apps/mobile/app/(protected)/home.tsx | 7 +- apps/mobile/app/(protected)/notifications.tsx | 4 +- .../features/account/AccountInfoContainer.tsx | 99 + .../features/account/AccountInfoScreen.tsx | 22 +- .../features/account/pushAvailability.ts | 17 + apps/mobile/features/account/types.ts | 34 + apps/mobile/features/api/accountClient.ts | 177 ++ .../mobile/features/api/notificationClient.ts | 89 + apps/mobile/features/auth/AuthProvider.tsx | 6 +- .../notifications/NotificationsContainer.tsx | 83 + .../notifications/NotificationsScreen.tsx | 65 +- apps/mobile/lib/apiConfig.ts | 45 + docs/plans/account-info-page.md | 17 +- infrastructure_plan.md | 74 +- package-lock.json | 222 ++ package.json | 1 + ...52452_notification_channel_preferences.sql | 27 + ...20261019170000_harden_profile_creation.sql | 38 + ...261019173000_restrict_account_policies.sql | 23 + .../20261019180000_create_notifications.sql | 27 + 52 files changed, 2213 insertions(+), 2749 deletions(-) create mode 100644 .env.example create mode 100644 .gitattributes delete mode 100644 .idea/.gitignore delete mode 100644 .idea/caches/deviceStreaming.xml delete mode 100644 .idea/deviceManager.xml delete mode 100644 .idea/markdown.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/quickcop.iml delete mode 100644 .idea/vcs.xml create mode 100644 apps/api/src/notifications/dto.ts create mode 100644 apps/api/src/notifications/notifications.controller.ts create mode 100644 apps/api/src/notifications/notifications.module.ts create mode 100644 apps/api/src/notifications/notifications.service.ts create mode 100644 apps/mobile/__tests__/account-info-container.test.tsx create mode 100644 apps/mobile/__tests__/api-config.test.ts create mode 100644 apps/mobile/__tests__/notification-client.test.ts create mode 100644 apps/mobile/__tests__/notifications-container.test.tsx create mode 100644 apps/mobile/features/account/AccountInfoContainer.tsx create mode 100644 apps/mobile/features/account/pushAvailability.ts create mode 100644 apps/mobile/features/api/accountClient.ts create mode 100644 apps/mobile/features/api/notificationClient.ts create mode 100644 apps/mobile/features/notifications/NotificationsContainer.tsx create mode 100644 apps/mobile/lib/apiConfig.ts create mode 100644 supabase/migrations/20261019152452_notification_channel_preferences.sql create mode 100644 supabase/migrations/20261019170000_harden_profile_creation.sql create mode 100644 supabase/migrations/20261019173000_restrict_account_policies.sql create mode 100644 supabase/migrations/20261019180000_create_notifications.sql diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e521fd9 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Local PostgreSQL service only. Replace only in an untracked .env file. +POSTGRES_DB=quickcop_dev +POSTGRES_USER=quickcop_dev +POSTGRES_PASSWORD=change-me-local-only +POSTGRES_PORT=5432 + +# Future API configuration: supply real values through a secret manager, never Git. +DATABASE_URL=postgresql://quickcop_dev:change-me-local-only@postgres:5432/quickcop_dev +ADDRESS_ENCRYPTION_KEY=replace-with-a-secret-manager-reference diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..cc2d335 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sql text eol=lf diff --git a/.gitignore b/.gitignore index ba32876..700225f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,8 @@ reports/ android/.gradle/ postgres-data/ *.log + +.idea/ +.env +.env.* +!.env.example diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index eaf91e2..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml deleted file mode 100644 index 7010156..0000000 --- a/.idea/caches/deviceStreaming.xml +++ /dev/null @@ -1,2385 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml deleted file mode 100644 index 81c3e56..0000000 --- a/.idea/deviceManager.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml deleted file mode 100644 index b1f3d01..0000000 --- a/.idea/markdown.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 6b4858d..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/quickcop.iml b/.idea/quickcop.iml deleted file mode 100644 index 18ec59d..0000000 --- a/.idea/quickcop.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index c8397c9..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index dc20f16..78ab8c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,12 +2,12 @@ ## Status and source of truth -This repository has infrastructure only. Read `infrastructure_plan.md` before changing tooling, hosting, security, database, release, or stack choices. The plan selects Expo/React Native Android, NestJS, PostgreSQL, Docker for local services, Cloud Run API/worker services (`min-instances >= 1`), and Cloud SQL private IP. Revise the plan with `infra-planner` before changing any of those decisions. +This repository contains infrastructure plus initial mobile/API slices. Read `infrastructure_plan.md` before changing tooling, hosting, security, database, release, or stack choices. The plan selects Expo/React Native Android, NestJS, Supabase-managed PostgreSQL, Docker/Supabase CLI for local services, and Cloud Run API/worker services (`min-instances >= 1`). Revise the plan with `infra-planner` before changing any of those decisions. ## Repository map -- Front end/mobile: `apps/mobile` — not created yet. -- API/backend: `apps/api` — not created yet. +- Front end/mobile: `apps/mobile` — Expo/React Native foundation and account/auth slices. +- API/backend: `apps/api` — NestJS/Supabase account boundary and tests; broader application behavior remains incomplete. - Infrastructure: `compose.yaml`, `docker/`, `.env.example`. - Tooling/tests: `package.json`, TypeScript/ESLint/Jest/Stryker configs, `scripts/`, `tests/`. - CI: `.github/workflows/pr-checks.yml` and `.github/workflows/release.yml`. @@ -18,7 +18,7 @@ This repository has infrastructure only. Read `infrastructure_plan.md` before ch - Infrastructure work must not add product screens, API routes, domain models, auth, checkout logic, or production data. - Do not commit secrets, `.env` files, certificates, reports, generated artifacts, or local database data. -- The API Dockerfile is intentionally unbuildable until application-owned `apps/api` exists; do not add a fake entrypoint merely to make it build. +- The API Dockerfile remains infrastructure-owned and must not be made buildable with a fake entrypoint; application-owned API packaging must be coordinated with the existing `apps/api` boundary. - Only approved partner APIs may later support checkout. Never collect or store card numbers/CVV. ## Skills and workflow @@ -29,7 +29,7 @@ This repository has infrastructure only. Read `infrastructure_plan.md` before ch ## Verification and lifecycle -After installing with `npm ci` on Node 22.14.0, run `npm run verify`, `npm run audit`, and `npm run test:smoke`. CI mirrors the static checks and coverage harness; it additionally runs Gitleaks and CodeQL. `npm run test:smoke` starts then removes the PostgreSQL service and volume. For manual local use, start with `npm run docker:up` and always clean up with `npm run docker:down`. +After installing with `npm ci` on Node 22.14.0, verify root and application packages independently: root `npm ci`, `npm test -- --runInBand`, and `npm run typecheck`; then `cd apps/mobile`, run the same three commands. Run API package tests/typecheck through its own package scripts as they are added. Also run `npm run verify`, `npm run audit`, and `npm run test:smoke` for the root harness. The Compose smoke test is not a Supabase-local-stack check; report those environments separately. ## Change checklist diff --git a/README.md b/README.md index 063a0bf..156bbda 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # QuickCop -Android-first stock tracking and partner-retailer checkout is planned here. The repository currently contains its development infrastructure only; no mobile app, NestJS API, authentication, product model, or checkout behavior has been implemented. +Android-first stock tracking and partner-retailer checkout is planned here. The repository contains the development infrastructure plus initial mobile authentication/account UI and a NestJS/Supabase account API slice; product monitoring, notifications, and checkout behavior remain incomplete. ## Repository map @@ -12,7 +12,7 @@ Android-first stock tracking and partner-retailer checkout is planned here. The - `.github/workflows/` — pull-request checks and guarded release prerequisite workflow. - `.agents/skills/` — project-supplied agent guidance. - `apps/mobile` — Expo/React Native Android foundation with local-only login and account-creation UI. -- `apps/api` — not created yet; application implementation owns it. +- `apps/api` — initial NestJS/Supabase account boundary and tests; application behavior is incomplete. ## Getting Started @@ -37,14 +37,21 @@ Android-first stock tracking and partner-retailer checkout is planned here. The docker run --rm --user "$(id -u):$(id -g)" -v "$PWD":/workspace -w /workspace node:22.14-bookworm-slim npm ci ``` -4. Run quality checks and the local database smoke test: +4. Verify the root package and then the mobile package independently: ```bash npm run verify + npm test -- --runInBand + npm run typecheck npm run test:smoke + + cd apps/mobile + npm ci + npm test -- --runInBand + npm run typecheck ``` - `npm test` and `npm run test:integration` validate an empty harness until application-owned tests exist. `npm run test:smoke` starts PostgreSQL, performs only `SELECT 1`, and stops/removes its local volume. + Each package's clean `npm ci` must succeed; root `npm ci` does not restore mobile or API dependencies. `npm run test:smoke` starts Compose PostgreSQL, performs only `SELECT 1`, and stops/removes its local volume. It does not validate the Supabase local stack used by application development. 5. For an interactive local database, run `npm run docker:up`; clean it up with `npm run docker:down`. @@ -54,13 +61,13 @@ The initial Android UI is in `apps/mobile`. It does not authenticate or persist ## Docker and deployment boundary -`compose.yaml` runs pinned `postgres:17.2-bookworm` locally and exposes it only on `127.0.0.1`. `docker/api.Dockerfile` is a hardened future NestJS image template and cannot build until `apps/api` is created by the application phase. +`compose.yaml` runs pinned `postgres:17.2-bookworm` locally and exposes it only on `127.0.0.1`; it is retained for the root smoke gate. Application development and Supabase migrations use the Supabase CLI/local stack. `docker/api.Dockerfile` remains a hardened API image template and must be coordinated with the existing `apps/api` application boundary. -Production is planned for Cloud Run API and worker services with at least one minimum instance each, plus private-IP Cloud SQL PostgreSQL with automated backups. The release workflow deliberately stops at prerequisites while mobile/API entrypoints and real protected-environment settings are absent; it does not publish, deploy, migrate, or contact Google Cloud. +Production is planned for Cloud Run API and worker services with at least one minimum instance each, plus a dedicated Supabase project providing managed PostgreSQL and backups. The release workflow deliberately stops at prerequisites while production credentials and protected-environment settings are absent; it does not publish, deploy, migrate, or contact cloud services. ## GitHub setup required later -Create a `production` protected environment and configure the plan-named `GCP_WORKLOAD_IDENTITY_PROVIDER`, `GCP_SERVICE_ACCOUNT`, `GCP_PROJECT_ID`, `GCP_REGION`, `CLOUD_SQL_INSTANCE`, `CLOUD_RUN_API_SERVICE`, and `CLOUD_RUN_WORKER_SERVICE` variables. Google Play publishing also needs its service-account JSON, Android signing key/password secrets, Google Cloud Workload Identity Federation, Artifact Registry, Cloud Run services, Cloud SQL instance, partner credentials, encryption-key management, and legal/privacy approval. +Create a `production` protected environment and configure the plan-named Google Cloud, Supabase, partner, encryption, and Android signing credentials. Google Play publishing also needs its service-account JSON and Android signing key/password secrets; Supabase migrations require protected project access and server-only credentials. ## Troubleshooting diff --git a/apps/api/src/account/account.service.ts b/apps/api/src/account/account.service.ts index d21a3b4..77ed47d 100644 --- a/apps/api/src/account/account.service.ts +++ b/apps/api/src/account/account.service.ts @@ -3,20 +3,44 @@ import { Injectable, InternalServerErrorException, NotFoundException, -} from "@nestjs/common"; -import { SupabaseService } from "../supabase/supabase.service"; -import { UpdatePreferencesDto, UpdateProfileDto } from "./dto"; - -const toProfile = (r: any) => ({ - username: r.username, - displayName: r.display_name, - phone: r.phone, +} from '@nestjs/common'; +import { SupabaseService } from '../supabase/supabase.service'; +import { UpdatePreferencesDto, UpdateProfileDto } from './dto'; + +type ProfileRow = { + username: string; + display_name: string | null; + phone: string | null; +}; + +type PreferencesRow = { + restock_push: boolean; + restock_email: boolean; + price_change_push: boolean; + price_change_email: boolean; +}; + +const PROFILE_COLUMNS = 'username, display_name, phone'; +const PREFERENCE_COLUMNS = + 'restock_push, restock_email, price_change_push, price_change_email'; + +const toProfile = (row: ProfileRow) => ({ + username: row.username, + displayName: row.display_name, + phone: row.phone, }); -const toPrefs = (r: any) => ({ - priceAlerts: r.price_alerts, - accountAlerts: r.account_alerts, + +const toPrefs = (row: PreferencesRow) => ({ + restock: { push: row.restock_push, email: row.restock_email }, + priceChange: { push: row.price_change_push, email: row.price_change_email }, }); +const normalizeOptionalText = (value: string | null | undefined) => { + if (value === undefined) return undefined; + if (value === null) return null; + return value.trim() || null; +}; + @Injectable() export class AccountService { constructor(private readonly supabase: SupabaseService) {} @@ -27,49 +51,98 @@ export class AccountService { async get(userId: string) { const [profile, prefs] = await Promise.all([ - this.db.from("profiles").select().eq("id", userId).maybeSingle(), - this.db.from("notification_preferences").select().eq("user_id", userId).maybeSingle(), + this.db + .from('profiles') + .select(PROFILE_COLUMNS) + .eq('id', userId) + .maybeSingle(), + this.db + .from('notification_preferences') + .select(PREFERENCE_COLUMNS) + .eq('user_id', userId) + .maybeSingle(), ]); if (profile.error || prefs.error) throw new InternalServerErrorException(); if (!profile.data || !prefs.data) throw new NotFoundException(); return { - profile: toProfile(profile.data), - notificationPreferences: toPrefs(prefs.data), + profile: toProfile(profile.data as ProfileRow), + notificationPreferences: toPrefs(prefs.data as PreferencesRow), }; } async updateProfile(userId: string, dto: UpdateProfileDto) { + const updates: Record = {}; + + if (dto.username !== undefined) { + updates.username = dto.username.trim().toLowerCase(); + } + if (dto.displayName !== undefined) { + updates.display_name = normalizeOptionalText(dto.displayName); + } + if (dto.phone !== undefined) { + updates.phone = normalizeOptionalText(dto.phone); + } + + if (Object.keys(updates).length === 0) { + const { data, error } = await this.db + .from('profiles') + .select(PROFILE_COLUMNS) + .eq('id', userId) + .maybeSingle(); + if (error) throw new InternalServerErrorException(); + if (!data) throw new NotFoundException(); + return toProfile(data as ProfileRow); + } + const { data, error } = await this.db - .from("profiles") - .update({ - username: dto.username, - display_name: dto.displayName, - phone: dto.phone, - updated_at: new Date().toISOString(), - }) - .eq("id", userId) - .select() - .single(); - - if (error?.code === "23505") throw new ConflictException("Username is taken."); - if (error || !data) throw new InternalServerErrorException(); - return toProfile(data); + .from('profiles') + .update({ ...updates, updated_at: new Date().toISOString() }) + .eq('id', userId) + .select(PROFILE_COLUMNS) + .maybeSingle(); + + if (error?.code === '23505') + throw new ConflictException('Username is taken.'); + if (error) throw new InternalServerErrorException(); + if (!data) throw new NotFoundException(); + return toProfile(data as ProfileRow); } async updatePreferences(userId: string, dto: UpdatePreferencesDto) { + const updates: Record = {}; + + if (dto.restock?.push !== undefined) + updates.restock_push = dto.restock.push; + if (dto.restock?.email !== undefined) + updates.restock_email = dto.restock.email; + if (dto.priceChange?.push !== undefined) { + updates.price_change_push = dto.priceChange.push; + } + if (dto.priceChange?.email !== undefined) { + updates.price_change_email = dto.priceChange.email; + } + + if (Object.keys(updates).length === 0) { + const { data, error } = await this.db + .from('notification_preferences') + .select(PREFERENCE_COLUMNS) + .eq('user_id', userId) + .maybeSingle(); + if (error) throw new InternalServerErrorException(); + if (!data) throw new NotFoundException(); + return toPrefs(data as PreferencesRow); + } + const { data, error } = await this.db - .from("notification_preferences") - .update({ - price_alerts: dto.priceAlerts, - account_alerts: dto.accountAlerts, - updated_at: new Date().toISOString(), - }) - .eq("user_id", userId) - .select() - .single(); - - if (error || !data) throw new InternalServerErrorException(); - return toPrefs(data); + .from('notification_preferences') + .update({ ...updates, updated_at: new Date().toISOString() }) + .eq('user_id', userId) + .select(PREFERENCE_COLUMNS) + .maybeSingle(); + + if (error) throw new InternalServerErrorException(); + if (!data) throw new NotFoundException(); + return toPrefs(data as PreferencesRow); } } diff --git a/apps/api/src/account/dto.ts b/apps/api/src/account/dto.ts index 9706c84..26559e3 100644 --- a/apps/api/src/account/dto.ts +++ b/apps/api/src/account/dto.ts @@ -1,21 +1,52 @@ -import { IsBoolean, IsOptional, IsString, Matches, MaxLength } from "class-validator"; +import { Transform, Type } from 'class-transformer'; +import { + IsBoolean, + IsOptional, + IsString, + Matches, + MaxLength, + ValidateNested, +} from 'class-validator'; export class UpdateProfileDto { @IsOptional() + @IsString() + @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) @Matches(/^[a-zA-Z0-9_]{3,30}$/, { - message: "username must be 3-30 letters, numbers or underscores", + message: 'username must be 3-30 letters, numbers or underscores', }) username?: string; - @IsOptional() @IsString() @MaxLength(50) - displayName?: string; + @IsOptional() + @IsString() + @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) + @MaxLength(50) + displayName?: string | null; @IsOptional() - @Matches(/^\+?[0-9]{7,15}$/, { message: "invalid phone number" }) - phone?: string; + @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) + @Matches(/^\+?[0-9]{7,15}$/, { message: 'invalid phone number' }) + phone?: string | null; +} + +class ChannelDto { + @IsOptional() + @IsBoolean() + push?: boolean; + + @IsOptional() + @IsBoolean() + email?: boolean; } export class UpdatePreferencesDto { - @IsOptional() @IsBoolean() priceAlerts?: boolean; - @IsOptional() @IsBoolean() accountAlerts?: boolean; + @IsOptional() + @ValidateNested() + @Type(() => ChannelDto) + restock?: ChannelDto; + + @IsOptional() + @ValidateNested() + @Type(() => ChannelDto) + priceChange?: ChannelDto; } diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 78cdf78..aa2a03d 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -1,8 +1,9 @@ -import { Module } from "@nestjs/common"; -import { AccountModule } from "./account/account.module"; -import { SupabaseModule } from "./supabase/supabase.module"; +import { Module } from '@nestjs/common'; +import { AccountModule } from './account/account.module'; +import { NotificationsModule } from './notifications/notifications.module'; +import { SupabaseModule } from './supabase/supabase.module'; @Module({ - imports: [SupabaseModule, AccountModule], + imports: [SupabaseModule, AccountModule, NotificationsModule], }) -export class AppModule {} \ No newline at end of file +export class AppModule {} diff --git a/apps/api/src/auth/supabase-auth.guard.ts b/apps/api/src/auth/supabase-auth.guard.ts index b141b15..289fe63 100644 --- a/apps/api/src/auth/supabase-auth.guard.ts +++ b/apps/api/src/auth/supabase-auth.guard.ts @@ -3,9 +3,9 @@ import { ExecutionContext, Injectable, UnauthorizedException, -} from "@nestjs/common"; -import { Request } from "express"; -import { SupabaseService } from "../supabase/supabase.service"; +} from '@nestjs/common'; +import { Request } from 'express'; +import { SupabaseService } from '../supabase/supabase.service'; export type AuthedRequest = Request & { user: { id: string } }; @@ -15,13 +15,27 @@ export class SupabaseAuthGuard implements CanActivate { async canActivate(ctx: ExecutionContext): Promise { const req = ctx.switchToHttp().getRequest(); - const [scheme, token] = (req.headers.authorization ?? "").split(" "); - if (scheme !== "Bearer" || !token) throw new UnauthorizedException(); + const authorization = req.headers.authorization; + const parts = + typeof authorization === 'string' + ? authorization.trim().split(/\s+/) + : []; + if ( + parts.length !== 2 || + parts[0].toLowerCase() !== 'bearer' || + !parts[1] + ) { + throw new UnauthorizedException(); + } - const { data, error } = await this.supabase.admin.auth.getUser(token); - if (error || !data.user) throw new UnauthorizedException(); + try { + const { data, error } = await this.supabase.admin.auth.getUser(parts[1]); + if (error || !data.user) throw new UnauthorizedException(); - req.user = { id: data.user.id }; - return true; + req.user = { id: data.user.id }; + return true; + } catch { + throw new UnauthorizedException(); + } } } diff --git a/apps/api/src/notifications/dto.ts b/apps/api/src/notifications/dto.ts new file mode 100644 index 0000000..f6e8acb --- /dev/null +++ b/apps/api/src/notifications/dto.ts @@ -0,0 +1,6 @@ +import { IsBoolean } from 'class-validator'; + +export class UpdateNotificationDto { + @IsBoolean() + enabled!: boolean; +} diff --git a/apps/api/src/notifications/notifications.controller.ts b/apps/api/src/notifications/notifications.controller.ts new file mode 100644 index 0000000..76c1998 --- /dev/null +++ b/apps/api/src/notifications/notifications.controller.ts @@ -0,0 +1,33 @@ +import { + Body, + Controller, + Get, + Param, + Patch, + Req, + UseGuards, +} from '@nestjs/common'; +import type { AuthedRequest } from '../auth/supabase-auth.guard'; +import { SupabaseAuthGuard } from '../auth/supabase-auth.guard'; +import { UpdateNotificationDto } from './dto'; +import { NotificationsService } from './notifications.service'; + +@Controller('notifications') +@UseGuards(SupabaseAuthGuard) +export class NotificationsController { + constructor(private readonly notifications: NotificationsService) {} + + @Get() + list(@Req() req: AuthedRequest) { + return this.notifications.list(req.user.id); + } + + @Patch(':id') + update( + @Req() req: AuthedRequest, + @Param('id') id: string, + @Body() dto: UpdateNotificationDto, + ) { + return this.notifications.update(req.user.id, id, dto); + } +} diff --git a/apps/api/src/notifications/notifications.module.ts b/apps/api/src/notifications/notifications.module.ts new file mode 100644 index 0000000..adec656 --- /dev/null +++ b/apps/api/src/notifications/notifications.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { SupabaseAuthGuard } from '../auth/supabase-auth.guard'; +import { NotificationsController } from './notifications.controller'; +import { NotificationsService } from './notifications.service'; + +@Module({ + controllers: [NotificationsController], + providers: [NotificationsService, SupabaseAuthGuard], +}) +export class NotificationsModule {} diff --git a/apps/api/src/notifications/notifications.service.ts b/apps/api/src/notifications/notifications.service.ts new file mode 100644 index 0000000..b371edd --- /dev/null +++ b/apps/api/src/notifications/notifications.service.ts @@ -0,0 +1,69 @@ +import { + Injectable, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { SupabaseService } from '../supabase/supabase.service'; +import { UpdateNotificationDto } from './dto'; + +type NotificationRow = { + id: string; + kind: 'price_change' | 'restock'; + name: string; + retailer: string; + previous_price: string; + current_price: string; + target_price: string; + enabled: boolean; + created_at: string; +}; + +const NOTIFICATION_COLUMNS = + 'id, kind, name, retailer, previous_price, current_price, target_price, enabled, created_at'; + +const toNotification = (row: NotificationRow) => ({ + id: row.id, + kind: row.kind, + name: row.name, + retailer: row.retailer, + previousPrice: row.previous_price, + currentPrice: row.current_price, + targetPrice: row.target_price, + enabled: row.enabled, + createdAt: row.created_at, +}); + +@Injectable() +export class NotificationsService { + constructor(private readonly supabase: SupabaseService) {} + + private get db() { + return this.supabase.admin; + } + + async list(userId: string) { + const { data, error } = await this.db + .from('notifications') + .select(NOTIFICATION_COLUMNS) + .eq('user_id', userId) + .order('created_at', { ascending: false }) + .limit(100); + + if (error) throw new InternalServerErrorException(); + return (data as NotificationRow[]).map(toNotification); + } + + async update(userId: string, id: string, dto: UpdateNotificationDto) { + const { data, error } = await this.db + .from('notifications') + .update({ enabled: dto.enabled }) + .eq('id', id) + .eq('user_id', userId) + .select(NOTIFICATION_COLUMNS) + .maybeSingle(); + + if (error) throw new InternalServerErrorException(); + if (!data) throw new NotFoundException(); + return toNotification(data as NotificationRow); + } +} diff --git a/apps/api/test/account.e2e-spec.ts b/apps/api/test/account.e2e-spec.ts index 8143999..9a263a3 100644 --- a/apps/api/test/account.e2e-spec.ts +++ b/apps/api/test/account.e2e-spec.ts @@ -1,156 +1,380 @@ -import { INestApplication } from "@nestjs/common"; -import { Test } from "@nestjs/testing"; -import { createClient } from "@supabase/supabase-js"; -import request from "supertest"; -import { AppModule } from "../src/app.module"; -import { configureApp } from "../src/app.setup"; - -jest.setTimeout(120_000); - -const URL = process.env.SUPABASE_URL ?? "http://127.0.0.1:54321"; -const ANON = process.env.SUPABASE_ANON_KEY!; -const SERVICE = process.env.SUPABASE_SERVICE_ROLE_KEY!; - -const admin = createClient(URL, SERVICE, { auth: { persistSession: false } }); -const anon = createClient(URL, ANON, { auth: { persistSession: false } }); - -async function createUser(label: string) { - const suffix = `${Date.now()}${Math.floor(Math.random() * 1000)}`; - const email = `${label}${suffix}@example.com`; - const username = `${label}_${suffix}`.slice(0, 30); - - const created = await admin.auth.admin.createUser({ - email, - password: "password123", - email_confirm: true, - user_metadata: { username }, - }); - if (created.error) throw created.error; - - const { data, error } = await anon.auth.signInWithPassword({ - email, - password: "password123", - }); - if (error || !data.session) throw error; - - return { id: created.data.user.id, username, token: data.session.access_token }; -} - -async function buildApp() { - const moduleRef = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - const app = configureApp(moduleRef.createNestApplication()); - await app.init(); - return app; -} - -describe("account API", () => { - let app: INestApplication; - let userA: Awaited>; - let userB: Awaited>; - -beforeAll(async () => { - app = await buildApp(); - userA = await createUser("usera"); - userB = await createUser("userb"); -}, 120_000); - -afterAll(async () => { - await app?.close(); - if (userA) await admin.auth.admin.deleteUser(userA.id); - if (userB) await admin.auth.admin.deleteUser(userB.id); -}, 60_000); - - const auth = (token: string) => ({ Authorization: `Bearer ${token}` }); - - it("rejects a request with no token", async () => { - await request(app.getHttpServer()).get("/account").expect(401); - }); - - it("rejects an invalid token", async () => { - await request(app.getHttpServer()) - .get("/account") - .set(auth("garbage")) - .expect(401); - }); - - it("returns only the caller's own account", async () => { - await request(app.getHttpServer()) - .patch("/account/profile") - .set(auth(userA.token)) - .send({ displayName: "Alice" }) - .expect(200); - await request(app.getHttpServer()) - .patch("/account/profile") - .set(auth(userB.token)) - .send({ displayName: "Bob" }) - .expect(200); - - const a = await request(app.getHttpServer()) - .get("/account") - .set(auth(userA.token)) - .expect(200); - const b = await request(app.getHttpServer()) - .get("/account") - .set(auth(userB.token)) - .expect(200); - - expect(a.body.profile.displayName).toBe("Alice"); - expect(a.body.profile.username).toBe(userA.username); - expect(b.body.profile.displayName).toBe("Bob"); - expect(b.body.profile.username).toBe(userB.username); - }); - - it("cannot target another user by sending an id", async () => { - await request(app.getHttpServer()) - .patch("/account/profile") - .set(auth(userA.token)) - .send({ id: userB.id, displayName: "Hacked" }) - .expect(400); - - const b = await request(app.getHttpServer()) - .get("/account") - .set(auth(userB.token)) - .expect(200); - expect(b.body.profile.displayName).toBe("Bob"); - }); - - it("returns 409 for a duplicate username, ignoring case", async () => { - await request(app.getHttpServer()) - .patch("/account/profile") - .set(auth(userA.token)) - .send({ username: userB.username.toUpperCase() }) - .expect(409); - }); - - it("returns 400 for invalid notification fields", async () => { - await request(app.getHttpServer()) - .patch("/account/notification-preferences") - .set(auth(userA.token)) - .send({ priceAlerts: "yes" }) - .expect(400); - await request(app.getHttpServer()) - .patch("/account/notification-preferences") - .set(auth(userA.token)) - .send({ foo: true }) - .expect(400); - }); - - it("persists updates after the app restarts", async () => { - await request(app.getHttpServer()) - .patch("/account/notification-preferences") - .set(auth(userA.token)) - .send({ priceAlerts: false }) - .expect(200); - - await app.close(); - app = await buildApp(); - - const res = await request(app.getHttpServer()) - .get("/account") - .set(auth(userA.token)) - .expect(200); - expect(res.body.notificationPreferences.priceAlerts).toBe(false); - }); -}); \ No newline at end of file +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { createClient } from '@supabase/supabase-js'; +import request from 'supertest'; +import { AppModule } from '../src/app.module'; +import { configureApp } from '../src/app.setup'; + +jest.setTimeout(120_000); + +const URL = process.env.SUPABASE_URL ?? 'http://127.0.0.1:54321'; +const ANON = process.env.SUPABASE_ANON_KEY!; +const SERVICE = process.env.SUPABASE_SERVICE_ROLE_KEY!; + +const admin = createClient(URL, SERVICE, { auth: { persistSession: false } }); +const anon = createClient(URL, ANON, { auth: { persistSession: false } }); + +function userDatabaseClient(token: string) { + return createClient(URL, ANON, { + auth: { persistSession: false, autoRefreshToken: false }, + global: { headers: { Authorization: `Bearer ${token}` } }, + }); +} + +async function createUser(label: string) { + const suffix = `${Date.now()}${Math.floor(Math.random() * 1000)}`; + const email = `${label}${suffix}@example.com`; + const username = `${label}_${suffix}`.slice(0, 30); + + return createUserWithMetadata(email, username); +} + +async function createUserWithMetadata( + email: string, + username?: string, + metadata?: Record, +) { + const userMetadata = { + ...metadata, + ...(username === undefined ? {} : { username }), + }; + + const created = await admin.auth.admin.createUser({ + email, + password: 'password123', + email_confirm: true, + user_metadata: userMetadata, + }); + if (created.error) throw created.error; + + const { data, error } = await anon.auth.signInWithPassword({ + email, + password: 'password123', + }); + if (error || !data.session) throw error; + + return { + id: created.data.user.id, + username: username ?? '', + token: data.session.access_token, + }; +} + +async function buildApp() { + const moduleRef = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + const app = configureApp(moduleRef.createNestApplication()); + await app.init(); + return app; +} + +describe('account API', () => { + let app: INestApplication; + let userA: Awaited>; + let userB: Awaited>; + + beforeAll(async () => { + app = await buildApp(); + userA = await createUser('usera'); + userB = await createUser('userb'); + }, 120_000); + + afterAll(async () => { + await app?.close(); + if (userA) await admin.auth.admin.deleteUser(userA.id); + if (userB) await admin.auth.admin.deleteUser(userB.id); + }, 60_000); + + const auth = (token: string) => ({ Authorization: `Bearer ${token}` }); + + it('rejects a request with no token', async () => { + await request(app.getHttpServer()).get('/account').expect(401); + }); + + it('rejects an invalid token', async () => { + await request(app.getHttpServer()) + .get('/account') + .set(auth('garbage')) + .expect(401); + }); + + it('rejects malformed authorization headers', async () => { + for (const authorization of [ + 'Basic garbage', + 'Bearer', + 'Bearer garbage extra', + 'Bearer ', + ]) { + await request(app.getHttpServer()) + .get('/account') + .set('Authorization', authorization) + .expect(401); + } + }); + + it('rejects unauthenticated notification access', async () => { + await request(app.getHttpServer()).get('/notifications').expect(401); + }); + + it("returns only the caller's own account", async () => { + await request(app.getHttpServer()) + .patch('/account/profile') + .set(auth(userA.token)) + .send({ displayName: 'Alice' }) + .expect(200); + await request(app.getHttpServer()) + .patch('/account/profile') + .set(auth(userB.token)) + .send({ displayName: 'Bob' }) + .expect(200); + + const a = await request(app.getHttpServer()) + .get('/account') + .set(auth(userA.token)) + .expect(200); + const b = await request(app.getHttpServer()) + .get('/account') + .set(auth(userB.token)) + .expect(200); + + expect(a.body.profile.displayName).toBe('Alice'); + expect(a.body.profile.username).toBe(userA.username); + expect(b.body.profile.displayName).toBe('Bob'); + expect(b.body.profile.username).toBe(userB.username); + }); + + it('cannot target another user by sending an id', async () => { + await request(app.getHttpServer()) + .patch('/account/profile') + .set(auth(userA.token)) + .send({ id: userB.id, displayName: 'Hacked' }) + .expect(400); + + const b = await request(app.getHttpServer()) + .get('/account') + .set(auth(userB.token)) + .expect(200); + expect(b.body.profile.displayName).toBe('Bob'); + }); + + it('enforces row ownership through database RLS', async () => { + const clientA = userDatabaseClient(userA.token); + + const profileRead = await clientA + .from('profiles') + .select('id') + .eq('id', userB.id) + .maybeSingle(); + expect(profileRead.error).toBeNull(); + expect(profileRead.data).toBeNull(); + + const profileUpdate = await clientA + .from('profiles') + .update({ display_name: 'Should Not Persist' }) + .eq('id', userB.id) + .select('id') + .maybeSingle(); + expect(profileUpdate.error).toBeNull(); + expect(profileUpdate.data).toBeNull(); + + const preferenceRead = await clientA + .from('notification_preferences') + .select('user_id') + .eq('user_id', userB.id) + .maybeSingle(); + expect(preferenceRead.error).toBeNull(); + expect(preferenceRead.data).toBeNull(); + }); + + it('isolates notification history and updates to the authenticated user', async () => { + const inserted = await admin + .from('notifications') + .insert({ + user_id: userA.id, + kind: 'price_change', + name: 'Private test item', + retailer: 'Test retailer', + previous_price: '$20.00', + current_price: '$15.00', + target_price: '$15.00', + enabled: true, + }) + .select('id') + .single(); + if (inserted.error || !inserted.data) throw inserted.error; + + const own = await request(app.getHttpServer()) + .get('/notifications') + .set(auth(userA.token)) + .expect(200); + expect(own.body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: inserted.data.id, + name: 'Private test item', + }), + ]), + ); + + const otherUser = await request(app.getHttpServer()) + .get('/notifications') + .set(auth(userB.token)) + .expect(200); + expect(otherUser.body).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: inserted.data.id }), + ]), + ); + + await request(app.getHttpServer()) + .patch(`/notifications/${inserted.data.id}`) + .set(auth(userB.token)) + .send({ enabled: false }) + .expect(404); + + await request(app.getHttpServer()) + .patch(`/notifications/${inserted.data.id}`) + .set(auth(userA.token)) + .send({ enabled: false }) + .expect(200); + }); + + it('returns 409 for a duplicate username, ignoring case', async () => { + await request(app.getHttpServer()) + .patch('/account/profile') + .set(auth(userA.token)) + .send({ username: userB.username.toUpperCase() }) + .expect(409); + }); + + it('normalizes profile PATCH values and preserves omitted fields', async () => { + const before = await request(app.getHttpServer()) + .get('/account') + .set(auth(userA.token)) + .expect(200); + + const updated = await request(app.getHttpServer()) + .patch('/account/profile') + .set(auth(userA.token)) + .send({ username: ' Normalized_User ' }) + .expect(200); + + expect(updated.body.username).toBe('normalized_user'); + + const after = await request(app.getHttpServer()) + .get('/account') + .set(auth(userA.token)) + .expect(200); + expect(after.body.profile.displayName).toBe( + before.body.profile.displayName, + ); + expect(after.body.profile.phone).toBe(before.body.profile.phone); + }); + + it('preserves omitted notification preference fields', async () => { + await request(app.getHttpServer()) + .patch('/account/notification-preferences') + .set(auth(userB.token)) + .send({ restock: { push: true } }) + .expect(200); + + const updated = await request(app.getHttpServer()) + .patch('/account/notification-preferences') + .set(auth(userB.token)) + .send({ restock: { email: true } }) + .expect(200); + + expect(updated.body.restock.push).toBe(true); + expect(updated.body.restock.email).toBe(true); + }); + + it('normalizes username and optional profile metadata during signup', async () => { + const suffix = `${Date.now()}${Math.floor(Math.random() * 1000)}`; + const created = await createUserWithMetadata( + `normalized${suffix}@example.com`, + ` Mixed_Name_${suffix} `, + { display_name: ' Example User ', phone: ' +14155550123 ' }, + ); + + try { + const account = await request(app.getHttpServer()) + .get('/account') + .set(auth(created.token)) + .expect(200); + + expect(account.body.profile.username).toBe(`mixed_name_${suffix}`); + expect(account.body.profile.displayName).toBe('Example User'); + expect(account.body.profile.phone).toBe('+14155550123'); + } finally { + await admin.auth.admin.deleteUser(created.id); + } + }); + + it('rejects signup without a valid username', async () => { + const suffix = `${Date.now()}${Math.floor(Math.random() * 1000)}`; + const created = await admin.auth.admin.createUser({ + email: `missing${suffix}@example.com`, + password: 'password123', + email_confirm: true, + }); + + expect(created.data.user).toBeNull(); + expect(created.error).toBeTruthy(); + }); + + it('rejects invalid phone metadata during signup', async () => { + const suffix = `${Date.now()}${Math.floor(Math.random() * 1000)}`; + const created = await admin.auth.admin.createUser({ + email: `phone${suffix}@example.com`, + password: 'password123', + email_confirm: true, + user_metadata: { username: `phone_${suffix}`, phone: 'not-a-phone' }, + }); + + expect(created.data.user).toBeNull(); + expect(created.error).toBeTruthy(); + }); + + it('returns 400 for invalid notification fields', async () => { + // wrong type on a real field + await request(app.getHttpServer()) + .patch('/account/notification-preferences') + .set(auth(userA.token)) + .send({ priceChange: { push: 'yes' } }) + .expect(400); + // unknown top-level field + await request(app.getHttpServer()) + .patch('/account/notification-preferences') + .set(auth(userA.token)) + .send({ foo: true }) + .expect(400); + // unknown nested field + await request(app.getHttpServer()) + .patch('/account/notification-preferences') + .set(auth(userA.token)) + .send({ restock: { sms: true } }) + .expect(400); + }); + + it('persists updates after the app restarts', async () => { + await request(app.getHttpServer()) + .patch('/account/notification-preferences') + .set(auth(userA.token)) + .send({ restock: { push: true } }) + .expect(200); + + await app.close(); + app = await buildApp(); + + const res = await request(app.getHttpServer()) + .get('/account') + .set(auth(userA.token)) + .expect(200); + expect(res.body.notificationPreferences.restock.push).toBe(true); + // untouched channels keep their defaults (all off) + expect(res.body.notificationPreferences.restock.email).toBe(false); + expect(res.body.notificationPreferences.priceChange.push).toBe(false); + expect(res.body.notificationPreferences.priceChange.email).toBe(false); + }); +}); diff --git a/apps/mobile/.env.example b/apps/mobile/.env.example index 8c59d69..ca82ee8 100644 --- a/apps/mobile/.env.example +++ b/apps/mobile/.env.example @@ -1,2 +1,3 @@ EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_replace-me +EXPO_PUBLIC_API_URL=https://your-api.example.com diff --git a/apps/mobile/README.md b/apps/mobile/README.md index 21c2c8d..a8fef9c 100644 --- a/apps/mobile/README.md +++ b/apps/mobile/README.md @@ -19,9 +19,10 @@ Set these values in a local, uncommitted `apps/mobile/.env` file: ```dotenv EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_... +EXPO_PUBLIC_API_URL=https://your-api.example.com ``` -For local Supabase development, use the local project URL appropriate to the emulator (`http://10.0.2.2:54321` for Android). EAS builds should receive the same two public values through EAS environment variables. The app fails closed when either value is missing or invalid. +For local Supabase development, use the local project URL appropriate to the emulator (`http://10.0.2.2:54321` for Android). For the local API, use `http://10.0.2.2:3000` from Android. EAS builds should receive the public Supabase values and HTTPS API URL through EAS environment variables. The app fails closed when required configuration is missing or invalid. The login screen is the initial public route. Protected routes require a restored Supabase session; **Log out** clears the device session immediately and reports if remote revocation could not be completed. diff --git a/apps/mobile/__tests__/account-info-container.test.tsx b/apps/mobile/__tests__/account-info-container.test.tsx new file mode 100644 index 0000000..b61bc04 --- /dev/null +++ b/apps/mobile/__tests__/account-info-container.test.tsx @@ -0,0 +1,174 @@ +import { fireEvent, render, waitFor } from "@testing-library/react-native"; + +import { AccountInfoContainer } from "../features/account/AccountInfoContainer"; +import type { AccountInfo } from "../features/account/types"; +import { + AccountSaveError, + loadAccount, + saveAccount, +} from "../features/api/accountClient"; + +jest.mock("../features/api/accountClient", () => { + class AccountSaveError extends Error {} + return { + AccountSaveError, + loadAccount: jest.fn(), + saveAccount: jest.fn(), + }; +}); + +const mockedLoad = loadAccount as jest.MockedFunction; +const mockedSave = saveAccount as jest.MockedFunction; + +const loaded: Omit = { + email: "shopper@example.com", + notificationPreferences: { + priceChange: { email: true, push: true }, + restock: { email: true, push: true }, + }, + pendingEmail: null, + phone: null, + username: "quickcop_user", + verifiedEmail: "shopper@example.com", +}; + +async function renderContainer(onUnauthorized = jest.fn()) { + const screen = await render( + , + ); + return { screen, onUnauthorized }; +} + +describe("AccountInfoContainer", () => { + beforeEach(() => { + mockedLoad.mockReset(); + mockedSave.mockReset(); + }); + + it("shows a loading indicator while the account loads", async () => { + mockedLoad.mockReturnValue(new Promise(() => {})); + const { screen } = await renderContainer(); + + expect(screen.getByLabelText("Loading account")).toBeTruthy(); + }); + + it("renders the loaded account", async () => { + mockedLoad.mockResolvedValue({ ok: true, data: loaded }); + const { screen } = await renderContainer(); + + expect(await screen.findByDisplayValue("quickcop_user")).toBeTruthy(); + expect(screen.getByDisplayValue("shopper@example.com")).toBeTruthy(); + }); + + it("saves changes and shows a success message", async () => { + mockedLoad.mockResolvedValue({ ok: true, data: loaded }); + mockedSave.mockResolvedValue(); + const { screen } = await renderContainer(); + + await screen.findByDisplayValue("quickcop_user"); + await fireEvent.changeText(screen.getByLabelText("Username"), "new_name"); + await fireEvent.press(screen.getByRole("button", { name: "Save changes" })); + + await waitFor(() => { + expect(mockedSave).toHaveBeenCalledWith( + expect.objectContaining({ username: "new_name" }), + ); + }); + expect(await screen.findByText("Changes saved.")).toBeTruthy(); + }); + + it("shows an offline message and recovers on retry", async () => { + mockedLoad + .mockResolvedValueOnce({ ok: false, failure: { kind: "network" } }) + .mockResolvedValueOnce({ ok: true, data: loaded }); + const { screen } = await renderContainer(); + + expect( + await screen.findByText("Unable to connect. Try again."), + ).toBeTruthy(); + + await fireEvent.press(screen.getByRole("button", { name: "Retry" })); + + expect(await screen.findByDisplayValue("quickcop_user")).toBeTruthy(); + expect(mockedLoad).toHaveBeenCalledTimes(2); + }); + + it("shows a retryable error when loading rejects", async () => { + mockedLoad.mockRejectedValue(new Error("request failed")); + const { screen } = await renderContainer(); + + expect( + await screen.findByText("Unable to connect. Try again."), + ).toBeTruthy(); + expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy(); + }); + + it("uses the device push capability result", async () => { + mockedLoad.mockResolvedValue({ ok: true, data: loaded }); + const screen = await render( + "unavailable"} + onUnauthorized={jest.fn()} + />, + ); + + expect( + await screen.findByText( + "Push notifications are unavailable on this device.", + ), + ).toBeTruthy(); + }); + + it("shows the conflict message when the username is taken", async () => { + mockedLoad.mockResolvedValue({ ok: true, data: loaded }); + mockedSave.mockRejectedValue( + new AccountSaveError("That username is taken."), + ); + const { screen } = await renderContainer(); + + await screen.findByDisplayValue("quickcop_user"); + await fireEvent.changeText(screen.getByLabelText("Username"), "taken_name"); + await fireEvent.press(screen.getByRole("button", { name: "Save changes" })); + + expect(await screen.findByText("That username is taken.")).toBeTruthy(); + }); + + it("calls onUnauthorized when the session is invalid", async () => { + mockedLoad.mockResolvedValue({ + ok: false, + failure: { kind: "unauthorized" }, + }); + const { onUnauthorized } = await renderContainer(); + + await waitFor(() => { + expect(onUnauthorized).toHaveBeenCalledTimes(1); + }); + }); + + it("tells the user to confirm a changed email address", async () => { + mockedLoad.mockResolvedValue({ ok: true, data: loaded }); + mockedSave.mockResolvedValue(); + const { screen } = await renderContainer(); + + await screen.findByDisplayValue("quickcop_user"); + await fireEvent.changeText( + screen.getByLabelText("Email"), + "new@example.com", + ); + await fireEvent.press(screen.getByRole("button", { name: "Save changes" })); + + await waitFor(() => { + expect(mockedSave).toHaveBeenCalledWith( + expect.objectContaining({ + email: "new@example.com", + pendingEmail: "new@example.com", + }), + ); + }); + expect( + await screen.findByText( + "Saved. Check your email to confirm the new address.", + ), + ).toBeTruthy(); + }); +}); diff --git a/apps/mobile/__tests__/account-info.test.tsx b/apps/mobile/__tests__/account-info.test.tsx index a791db2..8df419c 100644 --- a/apps/mobile/__tests__/account-info.test.tsx +++ b/apps/mobile/__tests__/account-info.test.tsx @@ -1,8 +1,18 @@ import { fireEvent, render, waitFor } from "@testing-library/react-native"; +import { AccountSaveError } from "../features/api/accountClient"; import { AccountInfoScreen } from "../features/account/AccountInfoScreen"; import type { AccountInfo } from "../features/account/types"; +jest.mock("../features/api/accountClient", () => { + class AccountSaveError extends Error {} + return { + AccountSaveError, + loadAccount: jest.fn(), + saveAccount: jest.fn(), + }; +}); + const account: AccountInfo = { email: "shopper@example.com", notificationPreferences: { @@ -60,7 +70,7 @@ describe("AccountInfoScreen", () => { }); }); - expect(screen.getByText("Changes saved for this session.")).toBeTruthy(); + expect(await screen.findByText("Changes saved.")).toBeTruthy(); }); it("requires a username and a valid email before saving", async () => { @@ -75,4 +85,34 @@ describe("AccountInfoScreen", () => { expect(screen.getByText("Enter a username.")).toBeTruthy(); expect(screen.getByText("Enter a valid email address.")).toBeTruthy(); }); + + it("renders safe defaults when the server omits a preference group", async () => { + const incompleteAccount = { + ...account, + notificationPreferences: { priceChange: { email: true, push: true } }, + } as AccountInfo; + const screen = await render( + , + ); + + expect(screen.getByLabelText("Restock push alerts").props.value).toBe( + false, + ); + expect(screen.getByLabelText("Price-change push alerts").props.value).toBe( + true, + ); + }); + + it("shows the message from a failed save", async () => { + const onSave = jest + .fn, [AccountInfo]>() + .mockRejectedValue(new AccountSaveError("That username is taken.")); + const screen = await render( + , + ); + + await fireEvent.press(screen.getByRole("button", { name: "Save changes" })); + + expect(await screen.findByText("That username is taken.")).toBeTruthy(); + }); }); diff --git a/apps/mobile/__tests__/api-config.test.ts b/apps/mobile/__tests__/api-config.test.ts new file mode 100644 index 0000000..9c3586f --- /dev/null +++ b/apps/mobile/__tests__/api-config.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "@jest/globals"; + +import { ApiConfigurationError, readApiUrl } from "../lib/apiConfig"; + +describe("readApiUrl", () => { + it("requires the API URL", () => { + expect(() => readApiUrl({})).toThrow("Missing EXPO_PUBLIC_API_URL."); + }); + + it("allows HTTPS API URLs and removes trailing slashes", () => { + expect( + readApiUrl({ EXPO_PUBLIC_API_URL: "https://api.quickcop.example///" }), + ).toBe("https://api.quickcop.example"); + }); + + it("allows HTTP only for local development hosts", () => { + expect( + readApiUrl({ + NODE_ENV: "development", + EXPO_PUBLIC_API_URL: "http://10.0.2.2:3000", + }), + ).toBe("http://10.0.2.2:3000"); + }); + + it("rejects HTTP outside local development", () => { + expect(() => + readApiUrl({ + NODE_ENV: "production", + EXPO_PUBLIC_API_URL: "http://api.quickcop.example", + }), + ).toThrow(ApiConfigurationError); + }); + + it("rejects credentials embedded in the API URL", () => { + expect(() => + readApiUrl({ + EXPO_PUBLIC_API_URL: "https://user:password@api.quickcop.example", + }), + ).toThrow(ApiConfigurationError); + }); +}); diff --git a/apps/mobile/__tests__/auth-flow.test.tsx b/apps/mobile/__tests__/auth-flow.test.tsx index 9a1aeaa..816872d 100644 --- a/apps/mobile/__tests__/auth-flow.test.tsx +++ b/apps/mobile/__tests__/auth-flow.test.tsx @@ -31,15 +31,20 @@ beforeEach(() => { }); describe("login", () => { - async function submitLogin(onLogin = jest.fn()) { - const user = userEvent.setup(); + async function submitLogin( + onLogin = jest.fn(), + email = "person@example.com", + ) { const screen = await render( , ); - await user.type(screen.getByLabelText("Email"), "person@example.com"); - await user.type(screen.getByLabelText("Password"), "password123"); - await user.press(screen.getByRole("button", { name: "Log in" })); + await fireEvent.changeText(screen.getByLabelText("Email"), email); + await fireEvent.changeText( + screen.getByLabelText("Password"), + "password123", + ); + await fireEvent.press(screen.getByRole("button", { name: "Log in" })); return { screen, onLogin }; } @@ -69,6 +74,23 @@ describe("login", () => { expect(onLogin).not.toHaveBeenCalled(); }); + it("uses the same login message for unknown emails and wrong passwords", async () => { + mockAuthService.signInWithEmail.mockResolvedValue({ + ok: false, + failure: { kind: "credentials" }, + }); + + const first = await submitLogin(jest.fn(), "unknown@example.com"); + expect( + await first.screen.findByText("Invalid email or password."), + ).toBeTruthy(); + + const second = await submitLogin(jest.fn(), "person@example.com"); + expect( + await second.screen.findByText("Invalid email or password."), + ).toBeTruthy(); + }); + it("shows a retryable message when the network request fails", async () => { mockAuthService.signInWithEmail.mockResolvedValue({ ok: false, diff --git a/apps/mobile/__tests__/auth-service.test.ts b/apps/mobile/__tests__/auth-service.test.ts index 3be5769..77958ae 100644 --- a/apps/mobile/__tests__/auth-service.test.ts +++ b/apps/mobile/__tests__/auth-service.test.ts @@ -52,4 +52,22 @@ describe("authService", () => { expect(mockAuth.signOut).toHaveBeenNthCalledWith(1, { scope: "global" }); expect(mockAuth.signOut).toHaveBeenNthCalledWith(2, { scope: "local" }); }); + + it("reports remote logout failure without leaving local auth uncleared", async () => { + mockAuth.signOut + .mockResolvedValueOnce({ error: new Error("remote failure") }) + .mockResolvedValueOnce({ error: null }); + + await expect(signOut()).resolves.toEqual({ ok: false }); + expect(mockAuth.signOut).toHaveBeenNthCalledWith(2, { scope: "local" }); + }); + + it("still attempts remote revocation when local logout fails", async () => { + mockAuth.signOut + .mockResolvedValueOnce({ error: null }) + .mockResolvedValueOnce({ error: new Error("local failure") }); + + await expect(signOut()).resolves.toEqual({ ok: false }); + expect(mockAuth.signOut).toHaveBeenNthCalledWith(1, { scope: "global" }); + }); }); diff --git a/apps/mobile/__tests__/notification-client.test.ts b/apps/mobile/__tests__/notification-client.test.ts new file mode 100644 index 0000000..1503153 --- /dev/null +++ b/apps/mobile/__tests__/notification-client.test.ts @@ -0,0 +1,54 @@ +import { + loadNotifications, + updateNotification, +} from "../features/api/notificationClient"; + +jest.mock("../lib/supabase", () => ({ + supabase: { auth: { getSession: jest.fn() } }, +})); + +const mockGetSession = jest.requireMock("../lib/supabase").supabase.auth + .getSession as jest.Mock; + +describe("notification client", () => { + beforeEach(() => { + mockGetSession.mockResolvedValue({ + data: { session: { access_token: "token" } }, + }); + global.fetch = jest.fn(); + process.env.EXPO_PUBLIC_API_URL = "https://api.example.com"; + }); + + it("loads authenticated notifications", async () => { + (global.fetch as jest.Mock).mockResolvedValue({ + ok: true, + json: async () => [], + }); + + await loadNotifications(); + + expect(global.fetch).toHaveBeenCalledWith( + "https://api.example.com/notifications", + expect.objectContaining({ + headers: expect.objectContaining({ Authorization: "Bearer token" }), + }), + ); + }); + + it("persists an alert toggle", async () => { + (global.fetch as jest.Mock).mockResolvedValue({ + ok: true, + json: async () => ({ id: "alert-1", enabled: false }), + }); + + await updateNotification("alert-1", false); + + expect(global.fetch).toHaveBeenCalledWith( + "https://api.example.com/notifications/alert-1", + expect.objectContaining({ + method: "PATCH", + body: JSON.stringify({ enabled: false }), + }), + ); + }); +}); diff --git a/apps/mobile/__tests__/notifications-container.test.tsx b/apps/mobile/__tests__/notifications-container.test.tsx new file mode 100644 index 0000000..d009ed7 --- /dev/null +++ b/apps/mobile/__tests__/notifications-container.test.tsx @@ -0,0 +1,74 @@ +import { fireEvent, render } from "@testing-library/react-native"; + +import { NotificationsContainer } from "../features/notifications/NotificationsContainer"; +import { + loadNotifications, + updateNotification, +} from "../features/api/notificationClient"; + +jest.mock("../features/api/notificationClient", () => ({ + loadNotifications: jest.fn(), + updateNotification: jest.fn(), +})); + +const mockedLoad = loadNotifications as jest.MockedFunction< + typeof loadNotifications +>; +const mockedUpdate = updateNotification as jest.MockedFunction< + typeof updateNotification +>; + +const alert = { + id: "alert-1", + kind: "price_change" as const, + name: "Nintendo Switch 2", + retailer: "Target", + previousPrice: "$499.99", + currentPrice: "$449.99", + targetPrice: "$450.00", + enabled: true, + createdAt: "2026-10-19T00:00:00.000Z", +}; + +describe("NotificationsContainer", () => { + beforeEach(() => { + mockedLoad.mockReset(); + mockedUpdate.mockReset(); + }); + + it("loads alerts from the server and persists a toggle", async () => { + mockedLoad.mockResolvedValue({ ok: true, data: [alert] }); + mockedUpdate.mockResolvedValue({ + ok: true, + data: { ...alert, enabled: false }, + }); + + const screen = await render( + , + ); + + const toggle = await screen.findByRole("switch", { + name: "Nintendo Switch 2 price alert", + }); + await fireEvent(toggle, "valueChange", false); + + expect(mockedUpdate).toHaveBeenCalledWith("alert-1", false); + expect( + screen.getByRole("switch", { name: "Nintendo Switch 2 price alert" }) + .props.value, + ).toBe(false); + }); + + it("shows a retryable error when alerts fail to load", async () => { + mockedLoad.mockResolvedValue({ ok: false, failure: { kind: "network" } }); + + const screen = await render( + , + ); + + expect( + await screen.findByText("Unable to load notifications. Try again."), + ).toBeTruthy(); + expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy(); + }); +}); diff --git a/apps/mobile/__tests__/notifications-screen.test.tsx b/apps/mobile/__tests__/notifications-screen.test.tsx index 6305a71..09f05de 100644 --- a/apps/mobile/__tests__/notifications-screen.test.tsx +++ b/apps/mobile/__tests__/notifications-screen.test.tsx @@ -6,10 +6,28 @@ import { type PriceAlert, } from "../features/notifications/NotificationsScreen"; +const alerts: PriceAlert[] = [ + { + id: "switch-2", + kind: "price_change", + name: "Nintendo Switch 2", + retailer: "Target", + previousPrice: "$499.99", + currentPrice: "$449.99", + targetPrice: "$450.00", + enabled: true, + createdAt: "2026-10-19T00:00:00.000Z", + }, +]; + describe("NotificationsScreen", () => { - it("shows example price-drop alerts and lets the user toggle an alert locally", async () => { + it("shows a server price-drop alert and lets the user toggle it", async () => { const screen = await render( - , + , ); expect(screen.getByText("Nintendo Switch 2")).toBeTruthy(); @@ -31,11 +49,11 @@ describe("NotificationsScreen", () => { }); it("shows an empty state when no price alerts are available", async () => { - const alerts: PriceAlert[] = []; const screen = await render( , ); @@ -51,7 +69,9 @@ describe("NotificationsScreen", () => { const onManageDeliveryPreferences = jest.fn(); const screen = await render( , ); diff --git a/apps/mobile/app/(protected)/account.tsx b/apps/mobile/app/(protected)/account.tsx index 83a6110..89db320 100644 --- a/apps/mobile/app/(protected)/account.tsx +++ b/apps/mobile/app/(protected)/account.tsx @@ -1,28 +1,29 @@ -import { useState } from "react"; +import { useRouter } from "expo-router"; -import { AccountInfoScreen } from "../../features/account/AccountInfoScreen"; -import type { AccountInfo } from "../../features/account/types"; - -const prototypeAccount: AccountInfo = { - email: "shopper@example.com", - notificationPreferences: { - priceChange: { email: false, push: false }, - restock: { email: false, push: false }, - }, - pendingEmail: null, - phone: null, - pushAvailability: "unavailable", - username: "quickcop_user", - verifiedEmail: "shopper@example.com", -}; +import { AccountInfoContainer } from "../../features/account/AccountInfoContainer"; +import { useAuth } from "../../features/auth/AuthProvider"; export default function AccountRoute() { - const [account, setAccount] = useState(prototypeAccount); + const router = useRouter(); + const { logout } = useAuth(); return ( - setAccount(next)} + { + void logout() + .then((result) => { + router.replace({ + pathname: "/(public)", + params: result.ok ? undefined : { logout: "failed" }, + }); + }) + .catch(() => { + router.replace({ + pathname: "/(public)", + params: { logout: "failed" }, + }); + }); + }} /> ); } diff --git a/apps/mobile/app/(protected)/home.tsx b/apps/mobile/app/(protected)/home.tsx index 5084c1c..2bc6a84 100644 --- a/apps/mobile/app/(protected)/home.tsx +++ b/apps/mobile/app/(protected)/home.tsx @@ -11,7 +11,12 @@ export default function HomeRoute() { router.push("/(protected)/account")} onLogout={async () => { - const result = await logout(); + let result = { ok: false }; + try { + result = await logout(); + } catch { + // Keep the local route transition recoverable if logout unexpectedly throws. + } router.replace({ pathname: "/(public)", params: result.ok ? undefined : { logout: "failed" }, diff --git a/apps/mobile/app/(protected)/notifications.tsx b/apps/mobile/app/(protected)/notifications.tsx index 4d36831..adb9027 100644 --- a/apps/mobile/app/(protected)/notifications.tsx +++ b/apps/mobile/app/(protected)/notifications.tsx @@ -1,12 +1,12 @@ import { useRouter } from "expo-router"; -import { NotificationsScreen } from "../../features/notifications/NotificationsScreen"; +import { NotificationsContainer } from "../../features/notifications/NotificationsContainer"; export default function NotificationsRoute() { const router = useRouter(); return ( - router.push("/(protected)/account")} /> ); diff --git a/apps/mobile/features/account/AccountInfoContainer.tsx b/apps/mobile/features/account/AccountInfoContainer.tsx new file mode 100644 index 0000000..44b8fec --- /dev/null +++ b/apps/mobile/features/account/AccountInfoContainer.tsx @@ -0,0 +1,99 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { ActivityIndicator, Pressable, Text, View } from "react-native"; + +import { + loadAccount, + saveAccount, + type FailureKind, +} from "../api/accountClient"; +import { AccountInfoScreen } from "./AccountInfoScreen"; +import { getPushAvailability } from "./pushAvailability"; +import type { AccountInfo } from "./types"; + +type State = + | { status: "loading" } + | { status: "ready"; account: AccountInfo } + | { status: "error"; kind: FailureKind }; + +export function AccountInfoContainer({ + onUnauthorized, + getPushAvailability: resolvePushAvailability = getPushAvailability, +}: { + onUnauthorized: () => void; + getPushAvailability?: () => Promise; +}) { + const [state, setState] = useState({ status: "loading" }); + const unauthorizedRef = useRef(onUnauthorized); + unauthorizedRef.current = onUnauthorized; + const resolvePushAvailabilityRef = useRef(resolvePushAvailability); + resolvePushAvailabilityRef.current = resolvePushAvailability; + const mountedRef = useRef(true); + const requestIdRef = useRef(0); + + const load = useCallback(async () => { + const requestId = ++requestIdRef.current; + setState({ status: "loading" }); + + try { + const result = await loadAccount(); + if (!mountedRef.current || requestId !== requestIdRef.current) return; + + if (result.ok) { + let pushAvailability: AccountInfo["pushAvailability"] = "unavailable"; + try { + pushAvailability = await resolvePushAvailabilityRef.current(); + } catch { + // A capability check must not prevent account data from loading. + } + if (!mountedRef.current || requestId !== requestIdRef.current) return; + setState({ + status: "ready", + account: { ...result.data, pushAvailability }, + }); + } else if (result.failure.kind === "unauthorized") { + unauthorizedRef.current(); + } else { + setState({ status: "error", kind: result.failure.kind }); + } + } catch { + if (mountedRef.current && requestId === requestIdRef.current) { + setState({ status: "error", kind: "network" }); + } + } + }, []); + + useEffect(() => { + mountedRef.current = true; + void load(); + return () => { + mountedRef.current = false; + requestIdRef.current += 1; + }; + }, [load]); + + const handleSave = useCallback(async (next: AccountInfo) => { + await saveAccount(next); + if (mountedRef.current) setState({ status: "ready", account: next }); + }, []); + + if (state.status === "loading") { + return ; + } + + if (state.status === "error") { + return ( + + + {state.kind === "network" + ? "Unable to connect. Try again." + : "We couldn't load your account. Try again."} + + void load()}> + Retry + + + ); + } + + return ; +} diff --git a/apps/mobile/features/account/AccountInfoScreen.tsx b/apps/mobile/features/account/AccountInfoScreen.tsx index 7aa263b..82ee855 100644 --- a/apps/mobile/features/account/AccountInfoScreen.tsx +++ b/apps/mobile/features/account/AccountInfoScreen.tsx @@ -12,6 +12,8 @@ import { } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; +import { AccountSaveError } from "../api/accountClient"; +import { normalizeNotificationPreferences } from "./types"; import type { AccountInfo, AlertType, @@ -43,8 +45,8 @@ function preferenceLabel(alertType: AlertType, channel: DeliveryChannel) { export function AccountInfoScreen({ account, onSave }: AccountInfoScreenProps) { const [email, setEmail] = useState(account.email); const [phone, setPhone] = useState(account.phone ?? ""); - const [preferences, setPreferences] = useState( - account.notificationPreferences, + const [preferences, setPreferences] = useState(() => + normalizeNotificationPreferences(account.notificationPreferences), ); const [username, setUsername] = useState(account.username); const [errors, setErrors] = useState([]); @@ -95,9 +97,17 @@ export function AccountInfoScreen({ account, onSave }: AccountInfoScreenProps) { phone: phone.trim() || null, username: normalizedUsername, }); - setSaveMessage("Changes saved for this session."); - } catch { - setSaveMessage("We could not save your changes. Try again."); + setSaveMessage( + normalizedEmail !== account.email + ? "Saved. Check your email to confirm the new address." + : "Changes saved.", + ); + } catch (error) { + setSaveMessage( + error instanceof AccountSaveError + ? error.message + : "We could not save your changes. Try again.", + ); } finally { setIsSaving(false); } @@ -277,7 +287,7 @@ function Toggle({ onValueChange(alertType, channel, value)} - value={preferences[alertType][channel]} + value={preferences[alertType]?.[channel] ?? false} />
); diff --git a/apps/mobile/features/account/pushAvailability.ts b/apps/mobile/features/account/pushAvailability.ts new file mode 100644 index 0000000..42ef0dd --- /dev/null +++ b/apps/mobile/features/account/pushAvailability.ts @@ -0,0 +1,17 @@ +import { PermissionsAndroid, Platform } from "react-native"; + +import type { AccountInfo } from "./types"; + +export async function getPushAvailability(): Promise< + AccountInfo["pushAvailability"] +> { + if (Platform.OS !== "android") return "available"; + + try { + const permission = PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS; + const granted = await PermissionsAndroid.check(permission); + return granted ? "available" : "unavailable"; + } catch { + return "unavailable"; + } +} diff --git a/apps/mobile/features/account/types.ts b/apps/mobile/features/account/types.ts index a72c824..0be5201 100644 --- a/apps/mobile/features/account/types.ts +++ b/apps/mobile/features/account/types.ts @@ -6,6 +6,40 @@ export type NotificationPreferences = Record< Record >; +const defaultChannelPreferences = (): Record => ({ + email: false, + push: false, +}); + +export function normalizeNotificationPreferences( + value: unknown, +): NotificationPreferences { + const source = + value && typeof value === "object" + ? (value as Record) + : {}; + + const normalizeChannel = (channel: unknown) => { + const input = + channel && typeof channel === "object" + ? (channel as Record) + : {}; + return { + email: input.email === true, + push: input.push === true, + }; + }; + + return { + priceChange: source.priceChange + ? normalizeChannel(source.priceChange) + : defaultChannelPreferences(), + restock: source.restock + ? normalizeChannel(source.restock) + : defaultChannelPreferences(), + }; +} + export type AccountInfo = { email: string; notificationPreferences: NotificationPreferences; diff --git a/apps/mobile/features/api/accountClient.ts b/apps/mobile/features/api/accountClient.ts new file mode 100644 index 0000000..55173f2 --- /dev/null +++ b/apps/mobile/features/api/accountClient.ts @@ -0,0 +1,177 @@ +import { readApiUrl } from "../../lib/apiConfig"; +import { supabase } from "../../lib/supabase"; +import { withTimeout } from "../auth/authService"; +import { + normalizeNotificationPreferences, + type AccountInfo, + type NotificationPreferences, +} from "../account/types"; + +export type FailureKind = + | "configuration" + | "unauthorized" + | "conflict" + | "validation" + | "network" + | "unknown"; + +export type Result = + | { ok: true; data: T } + | { ok: false; failure: { kind: FailureKind } }; + +export class AccountSaveError extends Error {} + +type ApiAccount = { + profile: { + username: string; + displayName: string | null; + phone: string | null; + }; + notificationPreferences: NotificationPreferences; +}; + +const REQUEST_TIMEOUT_MS = 10_000; + +async function request( + path: string, + init: RequestInit = {}, +): Promise> { + if (!supabase) return { ok: false, failure: { kind: "configuration" } }; + + try { + const apiUrl = readApiUrl(); + const { data } = await withTimeout( + supabase.auth.getSession(), + REQUEST_TIMEOUT_MS, + "Session lookup timed out.", + ); + const token = data.session?.access_token; + if (!token) return { ok: false, failure: { kind: "unauthorized" } }; + + const res = await withTimeout( + fetch(`${apiUrl}${path}`, { + ...init, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }), + REQUEST_TIMEOUT_MS, + "Account request timed out.", + ); + if (res.ok) return { ok: true, data: (await res.json()) as T }; + + const kind: FailureKind = + res.status === 401 + ? "unauthorized" + : res.status === 409 + ? "conflict" + : res.status === 400 || res.status === 422 + ? "validation" + : "unknown"; + return { ok: false, failure: { kind } }; + } catch { + try { + readApiUrl(); + } catch { + return { ok: false, failure: { kind: "configuration" } }; + } + return { ok: false, failure: { kind: "network" } }; + } +} + +export async function loadAccount(): Promise< + Result> +> { + const api = await request("/account"); + if (!api.ok) return api; + + try { + // The email lives in Supabase Auth, not in our tables. + const { data, error } = await withTimeout( + supabase!.auth.getUser(), + REQUEST_TIMEOUT_MS, + "Session lookup timed out.", + ); + if (error || !data.user) { + return { ok: false, failure: { kind: "unauthorized" } }; + } + + const verifiedEmail = data.user.email ?? ""; + const pendingEmail = data.user.new_email ?? null; + + return { + ok: true, + data: { + username: api.data.profile.username, + phone: api.data.profile.phone, + notificationPreferences: normalizeNotificationPreferences( + api.data.notificationPreferences, + ), + verifiedEmail, + pendingEmail, + email: pendingEmail ?? verifiedEmail, + }, + }; + } catch { + return { ok: false, failure: { kind: "network" } }; + } +} + +export async function saveAccount(next: AccountInfo): Promise { + const profile = await request("/account/profile", { + method: "PATCH", + body: JSON.stringify({ username: next.username, phone: next.phone }), + }); + if (!profile.ok) { + throw new AccountSaveError( + profile.failure.kind === "conflict" + ? "That username is taken." + : profile.failure.kind === "network" + ? "Unable to connect. Try again." + : profile.failure.kind === "validation" + ? "Check your username (letters, numbers, underscores) and phone number (digits, optional +)." + : "We could not save your changes. Try again.", + ); + } + + const prefs = await request("/account/notification-preferences", { + method: "PATCH", + body: JSON.stringify(next.notificationPreferences), + }); + if (!prefs.ok) { + throw new AccountSaveError( + prefs.failure.kind === "network" + ? "Unable to connect. Try again." + : "We could not save your changes. Try again.", + ); + } + + // Email changes go through Supabase's confirmation flow, never our tables. + // Only start one if the address differs from what Supabase already has. + try { + const { data } = await withTimeout( + supabase!.auth.getUser(), + REQUEST_TIMEOUT_MS, + "Session lookup timed out.", + ); + const currentTarget = data.user?.new_email ?? data.user?.email; + if (next.email !== currentTarget) { + const { error } = await withTimeout( + supabase!.auth.updateUser({ email: next.email }), + REQUEST_TIMEOUT_MS, + "Email change timed out.", + ); + if (error) { + throw new AccountSaveError( + "Your other changes were saved, but we could not start the email change. Try again.", + ); + } + } + } catch (error) { + if (error instanceof AccountSaveError) throw error; + throw new AccountSaveError( + "Your other changes were saved, but we could not start the email change. Try again.", + ); + } +} diff --git a/apps/mobile/features/api/notificationClient.ts b/apps/mobile/features/api/notificationClient.ts new file mode 100644 index 0000000..9d607d4 --- /dev/null +++ b/apps/mobile/features/api/notificationClient.ts @@ -0,0 +1,89 @@ +import { readApiUrl } from "../../lib/apiConfig"; +import { supabase } from "../../lib/supabase"; +import { withTimeout } from "../auth/authService"; + +export type NotificationAlert = { + id: string; + kind: "price_change" | "restock"; + name: string; + retailer: string; + previousPrice: string; + currentPrice: string; + targetPrice: string; + enabled: boolean; + createdAt: string; +}; + +export type NotificationFailureKind = + | "configuration" + | "unauthorized" + | "network" + | "unknown"; + +export type NotificationResult = + | { ok: true; data: T } + | { ok: false; failure: { kind: NotificationFailureKind } }; + +const REQUEST_TIMEOUT_MS = 10_000; + +async function request( + path: string, + init?: RequestInit, +): Promise> { + if (!supabase) return { ok: false, failure: { kind: "configuration" } }; + + try { + const apiUrl = readApiUrl(); + const { data } = await withTimeout( + supabase.auth.getSession(), + REQUEST_TIMEOUT_MS, + "Session lookup timed out.", + ); + const token = data.session?.access_token; + if (!token) return { ok: false, failure: { kind: "unauthorized" } }; + + const response = await withTimeout( + fetch(`${apiUrl}${path}`, { + ...init, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }), + REQUEST_TIMEOUT_MS, + "Notification request timed out.", + ); + + if (response.ok) { + return { ok: true, data: (await response.json()) as T }; + } + + return { + ok: false, + failure: { + kind: response.status === 401 ? "unauthorized" : "unknown", + }, + }; + } catch { + try { + readApiUrl(); + } catch { + return { ok: false, failure: { kind: "configuration" } }; + } + return { ok: false, failure: { kind: "network" } }; + } +} + +export function loadNotifications() { + return request("/notifications"); +} + +export function updateNotification(id: string, enabled: boolean) { + return request( + `/notifications/${encodeURIComponent(id)}`, + { + method: "PATCH", + body: JSON.stringify({ enabled }), + }, + ); +} diff --git a/apps/mobile/features/auth/AuthProvider.tsx b/apps/mobile/features/auth/AuthProvider.tsx index af8d01e..6e2c1c7 100644 --- a/apps/mobile/features/auth/AuthProvider.tsx +++ b/apps/mobile/features/auth/AuthProvider.tsx @@ -38,7 +38,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const logout = useCallback(async () => { setAuthState({ status: "unauthenticated" }); - return signOut(); + try { + return await signOut(); + } catch { + return { ok: false }; + } }, []); useEffect(() => { diff --git a/apps/mobile/features/notifications/NotificationsContainer.tsx b/apps/mobile/features/notifications/NotificationsContainer.tsx new file mode 100644 index 0000000..2b21c3b --- /dev/null +++ b/apps/mobile/features/notifications/NotificationsContainer.tsx @@ -0,0 +1,83 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { ActivityIndicator, Pressable, Text, View } from "react-native"; + +import { + loadNotifications, + updateNotification, + type NotificationAlert, +} from "../api/notificationClient"; +import { NotificationsScreen } from "./NotificationsScreen"; + +type State = + | { status: "loading" } + | { status: "ready"; alerts: NotificationAlert[] } + | { status: "error" }; + +export function NotificationsContainer({ + onManageDeliveryPreferences, +}: { + onManageDeliveryPreferences: () => void; +}) { + const [state, setState] = useState({ status: "loading" }); + const mountedRef = useRef(true); + const requestIdRef = useRef(0); + + const load = useCallback(async () => { + const requestId = ++requestIdRef.current; + setState({ status: "loading" }); + const result = await loadNotifications(); + if (!mountedRef.current || requestId !== requestIdRef.current) return; + + if (result.ok) setState({ status: "ready", alerts: result.data }); + else setState({ status: "error" }); + }, []); + + useEffect(() => { + mountedRef.current = true; + void load(); + return () => { + mountedRef.current = false; + requestIdRef.current += 1; + }; + }, [load]); + + async function toggleAlert(id: string, enabled: boolean) { + const result = await updateNotification(id, enabled); + if (!result.ok) throw new Error("Unable to update notification."); + if (mountedRef.current) { + setState((current) => + current.status !== "ready" + ? current + : { + status: "ready", + alerts: current.alerts.map((alert) => + alert.id === id ? result.data : alert, + ), + }, + ); + } + } + + if (state.status === "loading") { + return ; + } + + if (state.status === "error") { + return ( + + Unable to load notifications. Try again. + void load()}> + Retry + + + ); + } + + return ( + + ); +} diff --git a/apps/mobile/features/notifications/NotificationsScreen.tsx b/apps/mobile/features/notifications/NotificationsScreen.tsx index 6395d2c..b14f1fb 100644 --- a/apps/mobile/features/notifications/NotificationsScreen.tsx +++ b/apps/mobile/features/notifications/NotificationsScreen.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Pressable, @@ -9,55 +9,46 @@ import { View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; +import type { NotificationAlert } from "../api/notificationClient"; -export type PriceAlert = { - id: string; - name: string; - retailer: string; - previousPrice: string; - currentPrice: string; - targetPrice: string; - enabled: boolean; -}; - -const exampleAlerts: PriceAlert[] = [ - { - id: "switch-2", - name: "Nintendo Switch 2", - retailer: "Target", - previousPrice: "$499.99", - currentPrice: "$449.99", - targetPrice: "$450.00", - enabled: true, - }, - { - id: "headphones", - name: "Sony WH-1000XM6", - retailer: "Amazon", - previousPrice: "$449.99", - currentPrice: "$398.00", - targetPrice: "$400.00", - enabled: true, - }, -]; +export type PriceAlert = NotificationAlert; type NotificationsScreenProps = { - alerts?: PriceAlert[]; + alerts: PriceAlert[]; onManageDeliveryPreferences: () => void; + onToggleAlert: (id: string, enabled: boolean) => Promise; }; export function NotificationsScreen({ - alerts = exampleAlerts, + alerts, onManageDeliveryPreferences, + onToggleAlert, }: NotificationsScreenProps) { const [priceAlerts, setPriceAlerts] = useState(alerts); + const [toggleError, setToggleError] = useState(""); + + useEffect(() => setPriceAlerts(alerts), [alerts]); - function setAlertEnabled(id: string, enabled: boolean) { + async function setAlertEnabled(id: string, enabled: boolean) { + const previous = priceAlerts.find((alert) => alert.id === id)?.enabled; + setToggleError(""); setPriceAlerts((currentAlerts) => currentAlerts.map((alert) => alert.id === id ? { ...alert, enabled } : alert, ), ); + try { + await onToggleAlert(id, enabled); + } catch { + if (previous !== undefined) { + setPriceAlerts((currentAlerts) => + currentAlerts.map((alert) => + alert.id === id ? { ...alert, enabled: previous } : alert, + ), + ); + } + setToggleError("Unable to update this alert. Try again."); + } } return ( @@ -108,6 +99,11 @@ export function NotificationsScreen({ )) )} + {toggleError ? ( + + {toggleError} + + ) : null} ) { + return ( + environment.NODE_ENV === "development" || environment.NODE_ENV === "test" + ); +} + +export function readApiUrl( + environment: Record = process.env, +): string { + const rawUrl = environment.EXPO_PUBLIC_API_URL?.trim(); + if (!rawUrl) { + throw new ApiConfigurationError("Missing EXPO_PUBLIC_API_URL."); + } + + let parsedUrl: URL; + try { + parsedUrl = new URL(rawUrl); + } catch { + throw new ApiConfigurationError("EXPO_PUBLIC_API_URL must be a valid URL."); + } + + const localDevelopmentUrl = + isDevelopmentBuild(environment) && + ["localhost", "127.0.0.1", "10.0.2.2"].includes(parsedUrl.hostname); + if (parsedUrl.protocol !== "https:" && !localDevelopmentUrl) { + throw new ApiConfigurationError( + "EXPO_PUBLIC_API_URL must use HTTPS outside local development.", + ); + } + + if (parsedUrl.username || parsedUrl.password) { + throw new ApiConfigurationError( + "EXPO_PUBLIC_API_URL must not contain credentials.", + ); + } + + return rawUrl.replace(/\/+$/, ""); +} diff --git a/docs/plans/account-info-page.md b/docs/plans/account-info-page.md index a81dedf..04489d6 100644 --- a/docs/plans/account-info-page.md +++ b/docs/plans/account-info-page.md @@ -33,7 +33,7 @@ The delivery preferences must never pause, remove, or otherwise change monitorin ## Existing Constraints - The mobile application is Expo/React Native in `apps/mobile` and currently has local-only auth prototype screens. -- `apps/api` does not yet exist. Durable profile data, authorization, event processing, and notification delivery require the future NestJS API and worker specified in `infrastructure_plan.md`. +- `apps/api` now exists as an initial NestJS/Supabase slice, but its current split account routes and partial persistence implementation do not yet satisfy this contract. Durable profile data, authorization, event processing, and notification delivery remain application-owned API/worker work. - PostgreSQL is the planned system of record. Cloud Run API and worker services remain separate, always-on services; this plan does not change that infrastructure decision. ## User Flow @@ -70,6 +70,7 @@ interface UpdateAccountInfoInput { - `GET /api/account` returns only the signed-in account's `AccountInfo`. - `PATCH /api/account` atomically validates and updates the supplied non-email fields and preferences; when email changes, it records the replacement as pending verification while retaining the prior verified delivery address, then returns canonical account state. +- The request and response types are shared or generated from one typed contract used by the NestJS API, mobile client, and tests. The existing `/account/profile` and `/account/notification-preferences` routes are transitional and must not remain as parallel public contracts. - An unauthenticated request returns `401`; a request cannot name or modify another account. Invalid input returns the project's consistent `422` validation error shape. - Email-address changes must use the identity provider's verified-email flow before becoming the active delivery address. The prior verified email remains active until verification completes; the exact managed-provider integration is deferred until the provider is selected. - A push preference represents delivery intent, not proof that a current device can receive push. The response must expose non-sensitive push availability so the client can present a remediation state. @@ -134,7 +135,7 @@ When `apps/api` exists, add focused API and worker integration commands to its d ### Task 2: Implement authenticated profile read/update behavior -**Description:** Add `GET /api/account` and atomic `PATCH /api/account`, using the authenticated identity as the only account selector. +**Description:** Replace the current split account endpoints with `GET /api/account` and atomic `PATCH /api/account`, using the authenticated identity as the only account selector. All reads and writes go through NestJS; the mobile public Supabase key must never query private account tables directly. **Acceptance criteria:** @@ -147,6 +148,7 @@ When `apps/api` exists, add focused API and worker integration commands to its d - [ ] API integration tests cover success, invalid input, unauthenticated access, cross-account access, and atomic update failure. - [ ] Security review confirms boundary validation and no PII logging. +- [ ] Account deletion/export and cancellation of a pending email change have defined API behavior and tests. **Dependencies:** Task 1; real authentication/session ownership in the API. @@ -190,6 +192,7 @@ When `apps/api` exists, add focused API and worker integration commands to its d - [ ] React Native Testing Library tests cover rendering, required-field validation, each toggle, retry, successful save, pending email verification, and push-unavailable remediation. - [ ] Screen-reader labels/state and touch targets are manually checked on an Android emulator. +- [ ] Route protection covers `/home`, `/account`, and `/notifications`; route-matrix tests cover unauthenticated access, authenticated access, authenticated navigation to login/signup, rejected session restoration, and logout while a protected screen is mounted. **Dependencies:** Task 2 contract (or a deliberately isolated typed repository mock for an earlier UI-only slice). @@ -209,6 +212,8 @@ When `apps/api` exists, add focused API and worker integration commands to its d - [ ] Focused mobile tests plus API/worker integration suites pass. - [ ] `npm run verify`, `npm run audit`, `npm run test:smoke`, `git diff --check`, and the mobile test/typecheck commands pass under Node 22.14.0. +- [ ] Timeout/cancellation tests cover login, session restoration, account loading, account save, notifications, and logout; request cleanup uses `try/catch/finally` where required. +- [ ] Supabase error objects and raw backend errors are never logged or returned to clients. **Dependencies:** Tasks 2 and 4. @@ -246,3 +251,11 @@ Tasks 3 and 4 may proceed in parallel after the contract and Task 2's ownership/ - The managed identity provider has not yet been selected, so the precise email-change re-verification flow is deferred. - Push-token registration implementation, email provider selection, and notification deduplication retention windows belong to their respective future delivery specifications. + +## Cross-cutting requirements carried into every implementation task + +- Username availability and lookup must not be client-callable. Do not restore `get_email_for_username`, `username_available`, or an equivalent RPC. Username uniqueness is enforced server-side with a generic conflict response, and tests cover enumeration attempts through both login and signup. +- Mobile configuration is validated by a testable parser. It requires non-blank public URL and publishable/anonymous key values, rejects known secret variable names, rejects service-role JWT claims where applicable, and never places server credentials in `EXPO_PUBLIC_*` variables or the bundle. +- Every private account table has RLS policies keyed to the authenticated user, even when the API uses server-only credentials. Ownership tests cover both API authorization and database policy behavior. +- Notification history has pagination or another explicit bounded-result limit before Task 10/notification implementation begins. +- E2E account confirmation uses the local Supabase mail-testing service or a production-incompatible test-only provider fixture. “Confirm or simulate confirmation” is not an acceptable test definition. diff --git a/infrastructure_plan.md b/infrastructure_plan.md index 51f7ca5..33d6d69 100644 --- a/infrastructure_plan.md +++ b/infrastructure_plan.md @@ -1,6 +1,63 @@ # Infrastructure Plan -> Planning only. This document describes future infrastructure work. No installations, configuration changes, containers, workflows, deployments, or other implementation files were created by the infrastructure-planning process. +> This is the infrastructure source of truth. The repository now contains initial mobile and API implementation slices in addition to the infrastructure harness; the phase plan below distinguishes existing work from required completion work. + +## Current baseline and plan corrections + +The repository is no longer infrastructure-only: `apps/mobile` has its own Expo package, lockfile, Jest configuration, and typecheck script; `apps/api` has its own NestJS package, lockfile, account module, Supabase service, JWT guard, migrations, and API tests. The root package remains an infrastructure and shared-quality harness. Root commands do not install, test, or typecheck either application package. + +The planned completion sequence is therefore: + +### Phase 0: Restore both package environments + +1. Verify each package's lockfile independently. The required clean-install and verification scope is: + + ```bash + npm ci + npm test -- --runInBand + npm run typecheck + + cd apps/mobile + npm ci + npm test -- --runInBand + npm run typecheck + ``` + + The acceptance criterion is: **each package's clean `npm ci` succeeds**. Root `npm ci` does not restore mobile or API dependencies. + +2. Resolve the local database strategy before relying on smoke checks. The selected strategy is to retain the existing Compose PostgreSQL smoke test for the root infrastructure gate and use the Supabase CLI/local stack for application development, migrations, and Supabase-backed mobile/API flows. Document the boundary explicitly: `npm run test:smoke` proves only Compose PostgreSQL readiness, while Supabase-backed checks must start and validate the local Supabase services. If this choice changes, update this plan and migrate the script and documentation together. + +### Phase 1: Establish infrastructure and authentication + +3. Establish the NestJS API boundary, Supabase project configuration, JWT validation, server-only credentials, migration workflow, and local test environment. This task includes creating/maintaining `apps/api`, linking local and hosted Supabase environments, loading server credentials only on the API, configuring the mobile API base URL, and documenting local versus hosted behavior. Bootstrap, migration, and request-boundary code must define `try/catch/finally` cleanup/error behavior and tests for failed configuration, failed migrations, and abandoned requests. + +4. Add safe mobile configuration. The parser must require a public Supabase URL and publishable/anonymous key, reject missing or blank values with a clear diagnostic, reject known secret variable names, reject service-role keys by inspecting JWT role/claims where applicable, and ensure no server credentials are present in `EXPO_PUBLIC_*` variables or the mobile bundle. Configuration initialization and auth/session consumers must use `try/catch/finally` so rejected setup cannot leave loading or cleanup state unresolved. + +5. Replace username lookup and enumeration-prone RPCs. No client-callable RPC may answer username lookup or availability questions. Enforce uniqueness server-side, return generic conflict responses, and handle signup/account creation through the approved API or a carefully designed Supabase flow. Login and signup tests must not reveal whether a username exists. + +6. Implement complete protected-route and session handling. Protect `/home`, `/account`, and `/notifications` using route-group or equivalent segment-based logic. Add a route matrix covering unauthenticated and authenticated access to every protected route, authenticated navigation to login/signup, rejected session restoration, and logout while a protected screen is mounted. + +### Phase 2: Account contract and persistence + +7. Use the canonical account contract `GET /api/account` and atomic `PATCH /api/account` across the API, mobile client, and tests. All account reads/writes go through NestJS; the public mobile key must never access private Supabase tables directly. Account load/save implementations must define `try/catch/finally` behavior for success, validation failure, timeout/cancellation, and cleanup, with tests proving no stale loading/submitting state remains. + +8. Add Supabase migrations, RLS, validation, and ownership tests. The API may use server credentials, but defense in depth requires per-user RLS policies. Define whether each operation relies on API authorization, RLS, or both; this plan requires both for private account data. Include account deletion/export and pending-email cancellation behavior. + +9. Connect the account screen to the typed API contract. Preserve pending-email semantics, validation, generic conflicts, push availability, and safe error states. + +### Phase 3: Notifications and logout + +10. Replace notification fixtures with bounded server data. Add pagination or another explicit bounded-result requirement before notification history is implemented. + +11. Harden request cancellation, logout, and error handling. Login, session restoration, account loading, account save, notifications, and logout require timeout/cancellation tests. Use `try/catch/finally` in the relevant flows and never log Supabase error objects or raw backend errors. + +### Phase 4: Verification + +12. Add security tests for authentication, ownership, username enumeration, secrets/configuration, RLS, PII handling, generic conflicts, deletion/export, and email-change cancellation. + +13. Add a local Supabase-backed Maestro flow. Confirmation must use either the local Supabase mail-testing service or a test-only provider fixture that cannot run in production; tests must prove the same confirmation behavior users receive and may not “simulate confirmation” ambiguously. + +14. Run root, mobile, API, audit, smoke, and static-analysis gates. At minimum, run the root and mobile commands in Phase 0, `cd apps/api && npm ci && npm test -- --runInBand && npm run typecheck` for the API package, plus root `npm run verify`, `npm run audit`, and `npm run test:smoke`. The root Compose smoke test and Supabase-local application checks must be reported separately so a passing Compose check cannot be mistaken for validation of the planned Supabase environment. ## 1. Project and User Experience @@ -170,15 +227,14 @@ These are developer-workstation prerequisites that will not be supplied by the p | `production` | Protected GitHub environment | Require approval and scope production secrets | | Google Play Console account | Account | Own Android distribution and release controls | -## 11. Planned Repository Artifacts - Not Created by This Skill +## 11. Planned Repository Artifacts - Current Status -- [ ] Application manifests and npm lockfile. -- [ ] Expo/React Native mobile application and NestJS backend project files. -- [ ] Test, coverage, Maestro, StrykerJS, Prettier, ESLint, TypeScript, Semgrep, and Gitleaks configuration. -- [ ] API Dockerfile, Compose configuration, `.dockerignore`, and environment templates. -- [ ] `.github/workflows/pr-checks.yml`. -- [ ] `.github/workflows/release.yml`. -- [ ] Deployment, database migration, Google Play, and signing configuration. +- [x] Root, mobile, and API application manifests and lockfiles exist; their independent verification remains a Phase 0 requirement. +- [x] Initial Expo/React Native mobile and NestJS/Supabase account slices exist; full product behavior remains incomplete. +- [x] Root TypeScript, Jest, StrykerJS, Prettier, and ESLint configuration exists; mobile/API gates and security-analysis coverage remain to be completed. +- [x] Compose PostgreSQL smoke configuration and an initial API Dockerfile exist; Supabase local configuration and final application-owned image wiring remain planned. +- [x] Initial pull-request workflow exists; it must be expanded to verify root, mobile, API, Supabase-local, and security gates. +- [x] Release workflow scaffolding exists; deployment, database migration, Google Play, and signing configuration remain protected future work. ## 12. Assumptions and Open Items diff --git a/package-lock.json b/package-lock.json index 18cdd1d..2ce4f41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "jest": "^29.7.0", "pg": "^8.13.1", "prettier": "^3.4.2", + "supabase": "^2.121.0", "testcontainers": "^10.16.0", "ts-jest": "^29.2.5", "typescript": "^5.7.3", @@ -840,6 +841,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@ecies/ciphers": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.6.tgz", + "integrity": "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==", + "dev": true, + "license": "MIT", + "engines": { + "bun": ">=1", + "deno": ">=2.7.10", + "node": ">=16" + }, + "peerDependencies": { + "@noble/ciphers": "^1.0.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", @@ -2066,6 +2082,48 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "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/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2295,6 +2353,118 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/@supabase/cli-darwin-arm64": { + "version": "2.121.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-darwin-arm64/-/cli-darwin-arm64-2.121.0.tgz", + "integrity": "sha512-ZvXKj3wW4Kq5/MrG2xdgZeOWETxmUC9jl16X/i/np7u5A/WLuC/L0+KKY5i0eCrTnNwWFpZdlWKzu/J9QgkPgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@supabase/cli-darwin-x64": { + "version": "2.121.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-darwin-x64/-/cli-darwin-x64-2.121.0.tgz", + "integrity": "sha512-s+bnLljdI5MYlt92rJnsIuVcTYtmJyydUB/FUsKm5KCyD+HpwnY1hL6m/+bKZQCwIHsql1dlmJRZ0HNZOZzuTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@supabase/cli-linux-arm64": { + "version": "2.121.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-linux-arm64/-/cli-linux-arm64-2.121.0.tgz", + "integrity": "sha512-JKvffWRuzS1+cVzgv1gWi2nWaCGvS22mMMwEBnSkRFD0BSdN1QHZ7yteI7uRg1HV9R0s+HCwg1rtjrHnAEE55Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@supabase/cli-linux-arm64-musl": { + "version": "2.121.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.121.0.tgz", + "integrity": "sha512-8fGnWhnnbf3zdV7mQvEHb4qa1LMTq/JpSdO109O/Lxy246Dwar8o5XrDgy7bBK8QLzvp0XH+p1ky+wxRGVFzpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@supabase/cli-linux-x64": { + "version": "2.121.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-linux-x64/-/cli-linux-x64-2.121.0.tgz", + "integrity": "sha512-87EhtLctkNtC51CPhyeLoSdoGGzCZ3iNbSdczdcSklbvg0RsJbU+mTDaF3v53EWuPhyM0VRYqZJZNnjI5ABLcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@supabase/cli-linux-x64-musl": { + "version": "2.121.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-linux-x64-musl/-/cli-linux-x64-musl-2.121.0.tgz", + "integrity": "sha512-diDbFsC/KRmnOGUYYBxmUqn6ccqB+jliesMfp821grDIVkq9ucqg/aUjCOsCyRbcs5uhgbbWgh9YWTuxfVpZog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@supabase/cli-windows-arm64": { + "version": "2.121.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-windows-arm64/-/cli-windows-arm64-2.121.0.tgz", + "integrity": "sha512-cHpGQTLXUm4Uk9NzIdv8oUvmsNq7mlAN/CmVqNYzMUJyqmFofOPETwYPShDV9apA+5k84oKU93tn3N4wv7B3yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@supabase/cli-windows-x64": { + "version": "2.121.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-windows-x64/-/cli-windows-x64-2.121.0.tgz", + "integrity": "sha512-uvWiQ3cGKrZhtkctXHJJY0r0L6D2s/Dbc8/CxOVDxlXIH5mG3f8ZbqwMs9eRbPDqifyYFlwl+BX+tY3aJosIxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4058,6 +4228,24 @@ "dev": true, "license": "MIT" }, + "node_modules/eciesjs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.5.0.tgz", + "integrity": "sha512-s0J9SEVYAEPg7J63GFMApLYzPH9VNIQIyC6s15JpnqVc0TqcKWdbgFlnAweEBRyMmko2dcs2sfC83Hj4J43tuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ecies/ciphers": "^0.2.6", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0" + }, + "engines": { + "bun": ">=1", + "deno": ">=2.7.10", + "node": ">=16" + } + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -6547,6 +6735,16 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jose": { + "version": "6.2.13", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.13.tgz", + "integrity": "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-md4": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", @@ -8451,6 +8649,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/supabase": { + "version": "2.121.0", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.121.0.tgz", + "integrity": "sha512-xffhOgD9rXsXLK+DQ+3Gq8b2KmavMjPHGuJr7lfVlD8vKK1HHFggYlmzdRECqPmOL8g3Yd5PN3URcUR8xWZcWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eciesjs": "^0.5.0", + "jose": "^6.2.10" + }, + "bin": { + "supabase": "dist/supabase.js" + }, + "optionalDependencies": { + "@supabase/cli-darwin-arm64": "2.121.0", + "@supabase/cli-darwin-x64": "2.121.0", + "@supabase/cli-linux-arm64": "2.121.0", + "@supabase/cli-linux-arm64-musl": "2.121.0", + "@supabase/cli-linux-x64": "2.121.0", + "@supabase/cli-linux-x64-musl": "2.121.0", + "@supabase/cli-windows-arm64": "2.121.0", + "@supabase/cli-windows-x64": "2.121.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index ad4dc9f..7e6d106 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "jest": "^29.7.0", "pg": "^8.13.1", "prettier": "^3.4.2", + "supabase": "^2.121.0", "testcontainers": "^10.16.0", "ts-jest": "^29.2.5", "typescript": "^5.7.3", diff --git a/supabase/migrations/20261019152452_notification_channel_preferences.sql b/supabase/migrations/20261019152452_notification_channel_preferences.sql new file mode 100644 index 0000000..7f3a4a6 --- /dev/null +++ b/supabase/migrations/20261019152452_notification_channel_preferences.sql @@ -0,0 +1,27 @@ +create table if not exists public.notification_preferences ( + user_id uuid primary key references auth.users(id) on delete cascade, + updated_at timestamptz not null default now() +); + +alter table public.notification_preferences + add column if not exists restock_push boolean not null default false, + add column if not exists restock_email boolean not null default false, + add column if not exists price_change_push boolean not null default false, + add column if not exists price_change_email boolean not null default false, + add column if not exists updated_at timestamptz not null default now(); + +alter table public.notification_preferences + drop column if exists price_alerts, + drop column if exists account_alerts; + +alter table public.notification_preferences enable row level security; + +drop policy if exists "users manage their notification preferences" +on public.notification_preferences; + +create policy "users manage their notification preferences" +on public.notification_preferences +for all +to authenticated +using (auth.uid() = user_id) +with check (auth.uid() = user_id); diff --git a/supabase/migrations/20261019170000_harden_profile_creation.sql b/supabase/migrations/20261019170000_harden_profile_creation.sql new file mode 100644 index 0000000..a2444f6 --- /dev/null +++ b/supabase/migrations/20261019170000_harden_profile_creation.sql @@ -0,0 +1,38 @@ +create or replace function public.handle_new_user() +returns trigger +language plpgsql +security definer set search_path = '' +as $$ +declare + normalized_username text; + normalized_phone text; +begin + normalized_username := lower(trim(new.raw_user_meta_data->>'username')); + + if normalized_username is null + or normalized_username !~ '^[a-z0-9_]{3,30}$' then + raise exception using + errcode = 'check_violation', + message = 'A valid username is required.'; + end if; + + normalized_phone := nullif(trim(new.raw_user_meta_data->>'phone'), ''); + if normalized_phone is not null + and normalized_phone !~ '^\+?[0-9]{7,15}$' then + raise exception using + errcode = 'check_violation', + message = 'The phone number is invalid.'; + end if; + + insert into public.profiles (id, username, display_name, phone) + values ( + new.id, + normalized_username, + nullif(trim(new.raw_user_meta_data->>'display_name'), ''), + normalized_phone + ); + + insert into public.notification_preferences (user_id) values (new.id); + return new; +end; +$$; diff --git a/supabase/migrations/20261019173000_restrict_account_policies.sql b/supabase/migrations/20261019173000_restrict_account_policies.sql new file mode 100644 index 0000000..1841cce --- /dev/null +++ b/supabase/migrations/20261019173000_restrict_account_policies.sql @@ -0,0 +1,23 @@ +alter table public.profiles enable row level security; + +drop policy if exists "users manage their profile" +on public.profiles; + +create policy "users manage their profile" +on public.profiles +for all +to authenticated +using (auth.uid() = id) +with check (auth.uid() = id); + +alter table public.notification_preferences enable row level security; + +drop policy if exists "users manage their notification preferences" +on public.notification_preferences; + +create policy "users manage their notification preferences" +on public.notification_preferences +for all +to authenticated +using (auth.uid() = user_id) +with check (auth.uid() = user_id); diff --git a/supabase/migrations/20261019180000_create_notifications.sql b/supabase/migrations/20261019180000_create_notifications.sql new file mode 100644 index 0000000..814599a --- /dev/null +++ b/supabase/migrations/20261019180000_create_notifications.sql @@ -0,0 +1,27 @@ +create table if not exists public.notifications ( + id uuid primary key default gen_random_uuid(), + user_id uuid not null references auth.users(id) on delete cascade, + kind text not null check (kind in ('price_change', 'restock')), + name text not null, + retailer text not null, + previous_price text not null, + current_price text not null, + target_price text not null, + enabled boolean not null default true, + created_at timestamptz not null default now() +); + +create index if not exists notifications_user_created_at_idx + on public.notifications (user_id, created_at desc); + +alter table public.notifications enable row level security; + +drop policy if exists "users manage their notifications" +on public.notifications; + +create policy "users manage their notifications" +on public.notifications +for all +to authenticated +using (auth.uid() = user_id) +with check (auth.uid() = user_id); From 440875a287a2f94c181f668002a0aeba507a9ea0 Mon Sep 17 00:00:00 2001 From: InsertUzername Date: Wed, 21 Oct 2026 11:10:27 -0700 Subject: [PATCH 09/12] docs: document current account API contract --- docs/plans/account-info-page.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/plans/account-info-page.md b/docs/plans/account-info-page.md index 04489d6..251ccae 100644 --- a/docs/plans/account-info-page.md +++ b/docs/plans/account-info-page.md @@ -33,7 +33,7 @@ The delivery preferences must never pause, remove, or otherwise change monitorin ## Existing Constraints - The mobile application is Expo/React Native in `apps/mobile` and currently has local-only auth prototype screens. -- `apps/api` now exists as an initial NestJS/Supabase slice, but its current split account routes and partial persistence implementation do not yet satisfy this contract. Durable profile data, authorization, event processing, and notification delivery remain application-owned API/worker work. +- `apps/api` now exists as an initial NestJS/Supabase slice. The account page currently uses three application boundaries: `GET /account`, `PATCH /account/profile`, and `PATCH /account/notification-preferences`. Durable profile data, authorization, event processing, and notification delivery remain application-owned API/worker work. - PostgreSQL is the planned system of record. Cloud Run API and worker services remain separate, always-on services; this plan does not change that infrastructure decision. ## User Flow @@ -45,9 +45,9 @@ The delivery preferences must never pause, remove, or otherwise change monitorin 5. Future stock-monitoring code records a restock or price decrease event independently of these preferences. Delivery code sends only through the currently enabled, available matching channel(s). With all four toggles off, the event is still recorded and monitoring continues, but no push or email is sent. 6. When push is enabled but unavailable, the app retains the choice and explains how to enable Android notifications; it must not silently switch the preference off. -## Proposed Contract +## Current Account Contract -Define the shared API contract before implementing either client or server. Do not expose password hashes, sessions, verification tokens, provider identifiers, or other internal security fields. +The current request and response contract must not expose password hashes, sessions, verification tokens, provider identifiers, or other internal security fields. ```ts type AlertKind = "RESTOCK" | "PRICE_CHANGE"; @@ -68,11 +68,13 @@ interface UpdateAccountInfoInput { } ``` -- `GET /api/account` returns only the signed-in account's `AccountInfo`. -- `PATCH /api/account` atomically validates and updates the supplied non-email fields and preferences; when email changes, it records the replacement as pending verification while retaining the prior verified delivery address, then returns canonical account state. -- The request and response types are shared or generated from one typed contract used by the NestJS API, mobile client, and tests. The existing `/account/profile` and `/account/notification-preferences` routes are transitional and must not remain as parallel public contracts. +- `GET /account` returns only the signed-in account's profile and notification preferences. +- `PATCH /account/profile` validates and updates the signed-in account's username, display name, and phone fields. +- `PATCH /account/notification-preferences` validates and updates the signed-in account's supported notification preference fields. +- Email updates go directly through Supabase Auth, so a profile change and an email change are not atomic. +- The request and response types are shared or generated from one typed contract used by the NestJS API, mobile client, and tests. - An unauthenticated request returns `401`; a request cannot name or modify another account. Invalid input returns the project's consistent `422` validation error shape. -- Email-address changes must use the identity provider's verified-email flow before becoming the active delivery address. The prior verified email remains active until verification completes; the exact managed-provider integration is deferred until the provider is selected. +- Email-address changes use Supabase Auth's verified-email flow before becoming the active delivery address. The prior verified email remains active until verification completes. - A push preference represents delivery intent, not proof that a current device can receive push. The response must expose non-sensitive push availability so the client can present a remediation state. ## Data and Worker Invariant @@ -135,7 +137,7 @@ When `apps/api` exists, add focused API and worker integration commands to its d ### Task 2: Implement authenticated profile read/update behavior -**Description:** Replace the current split account endpoints with `GET /api/account` and atomic `PATCH /api/account`, using the authenticated identity as the only account selector. All reads and writes go through NestJS; the mobile public Supabase key must never query private account tables directly. +**Description:** Implement authenticated profile and preference behavior through the current split account endpoints, using the authenticated identity as the only account selector. Profile and preference reads and writes go through NestJS; email updates use the Supabase Auth flow directly from the mobile client. **Acceptance criteria:** @@ -146,7 +148,7 @@ When `apps/api` exists, add focused API and worker integration commands to its d **Verification:** -- [ ] API integration tests cover success, invalid input, unauthenticated access, cross-account access, and atomic update failure. +- [ ] API integration tests cover success, invalid input, unauthenticated access, and cross-account access for each endpoint; mobile tests cover the non-atomic profile/email update behavior. - [ ] Security review confirms boundary validation and no PII logging. - [ ] Account deletion/export and cancellation of a pending email change have defined API behavior and tests. From f9f5c89405e6e08cd0e8560bc17317122eb68a3b Mon Sep 17 00:00:00 2001 From: InsertUzername Date: Wed, 21 Oct 2026 11:13:48 -0700 Subject: [PATCH 10/12] docs: align infrastructure account contract --- infrastructure_plan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure_plan.md b/infrastructure_plan.md index 33d6d69..59ac1a6 100644 --- a/infrastructure_plan.md +++ b/infrastructure_plan.md @@ -39,7 +39,7 @@ The planned completion sequence is therefore: ### Phase 2: Account contract and persistence -7. Use the canonical account contract `GET /api/account` and atomic `PATCH /api/account` across the API, mobile client, and tests. All account reads/writes go through NestJS; the public mobile key must never access private Supabase tables directly. Account load/save implementations must define `try/catch/finally` behavior for success, validation failure, timeout/cancellation, and cleanup, with tests proving no stale loading/submitting state remains. +7. Use the current three-endpoint account contract across the API, mobile client, and tests: `GET /account`, `PATCH /account/profile`, and `PATCH /account/notification-preferences`. Profile and notification-preference reads/writes go through NestJS; email updates go directly through Supabase Auth, so profile and email changes are not atomic. The public mobile key must never access private Supabase tables directly. Account load/save implementations must define `try/catch/finally` behavior for success, validation failure, timeout/cancellation, and cleanup, with tests proving no stale loading/submitting state remains. 8. Add Supabase migrations, RLS, validation, and ownership tests. The API may use server credentials, but defense in depth requires per-user RLS policies. Define whether each operation relies on API authorization, RLS, or both; this plan requires both for private account data. Include account deletion/export and pending-email cancellation behavior. From a623b88697115be4a8023002c29f08fa9d196f4a Mon Sep 17 00:00:00 2001 From: InsertUzername Date: Wed, 21 Oct 2026 12:14:20 -0700 Subject: [PATCH 11/12] chore(lint): ignore build output in nested packages --- .prettierignore | 1 + apps/api/eslint.config.mjs | 13 +++++++++++-- apps/api/src/account/account.controller.ts | 21 ++++++++++++--------- apps/api/src/account/account.module.ts | 8 ++++---- apps/api/src/app.setup.ts | 2 +- apps/api/src/main.ts | 12 ++++++------ apps/api/src/supabase/supabase.module.ts | 6 +++--- apps/api/src/supabase/supabase.service.ts | 10 +++++----- apps/api/test/setup-env.ts | 4 ++-- apps/mobile/__tests__/auth-flow.test.tsx | 7 +------ eslint.config.mjs | 11 ++++++++++- 11 files changed, 56 insertions(+), 39 deletions(-) diff --git a/.prettierignore b/.prettierignore index 2bcc58d..defbdf8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,3 +3,4 @@ coverage dist package-lock.json .agents/ +supabase/.temp \ No newline at end of file diff --git a/apps/api/eslint.config.mjs b/apps/api/eslint.config.mjs index 4e9f827..425d2a5 100644 --- a/apps/api/eslint.config.mjs +++ b/apps/api/eslint.config.mjs @@ -6,7 +6,16 @@ import tseslint from 'typescript-eslint'; export default tseslint.config( { - ignores: ['eslint.config.mjs'], + ignores: [ + 'coverage/**', + 'node_modules/**', + 'dist/**', + '**/dist/**', + '**/node_modules/**', + '**/coverage/**', + '**/.expo/**', + 'supabase/.temp', + ], }, eslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, @@ -29,7 +38,7 @@ export default tseslint.config( '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-floating-promises': 'warn', '@typescript-eslint/no-unsafe-argument': 'warn', - "prettier/prettier": ["error", { endOfLine: "auto" }], + 'prettier/prettier': ['error', { endOfLine: 'auto' }], }, }, ); diff --git a/apps/api/src/account/account.controller.ts b/apps/api/src/account/account.controller.ts index 623d54d..b32b4f7 100644 --- a/apps/api/src/account/account.controller.ts +++ b/apps/api/src/account/account.controller.ts @@ -1,9 +1,9 @@ -import { Body, Controller, Get, Patch, Req, UseGuards } from "@nestjs/common"; -import type { AuthedRequest } from "../auth/supabase-auth.guard"; -import { SupabaseAuthGuard } from "../auth/supabase-auth.guard"; -import { AccountService } from "./account.service"; -import { UpdatePreferencesDto, UpdateProfileDto } from "./dto"; -@Controller("account") +import { Body, Controller, Get, Patch, Req, UseGuards } from '@nestjs/common'; +import type { AuthedRequest } from '../auth/supabase-auth.guard'; +import { SupabaseAuthGuard } from '../auth/supabase-auth.guard'; +import { AccountService } from './account.service'; +import { UpdatePreferencesDto, UpdateProfileDto } from './dto'; +@Controller('account') @UseGuards(SupabaseAuthGuard) export class AccountController { constructor(private readonly account: AccountService) {} @@ -13,13 +13,16 @@ export class AccountController { return this.account.get(req.user.id); } - @Patch("profile") + @Patch('profile') updateProfile(@Req() req: AuthedRequest, @Body() dto: UpdateProfileDto) { return this.account.updateProfile(req.user.id, dto); } - @Patch("notification-preferences") - updatePreferences(@Req() req: AuthedRequest, @Body() dto: UpdatePreferencesDto) { + @Patch('notification-preferences') + updatePreferences( + @Req() req: AuthedRequest, + @Body() dto: UpdatePreferencesDto, + ) { return this.account.updatePreferences(req.user.id, dto); } } diff --git a/apps/api/src/account/account.module.ts b/apps/api/src/account/account.module.ts index bdb4353..12b20e8 100644 --- a/apps/api/src/account/account.module.ts +++ b/apps/api/src/account/account.module.ts @@ -1,7 +1,7 @@ -import { Module } from "@nestjs/common"; -import { SupabaseAuthGuard } from "../auth/supabase-auth.guard"; -import { AccountController } from "./account.controller"; -import { AccountService } from "./account.service"; +import { Module } from '@nestjs/common'; +import { SupabaseAuthGuard } from '../auth/supabase-auth.guard'; +import { AccountController } from './account.controller'; +import { AccountService } from './account.service'; @Module({ controllers: [AccountController], diff --git a/apps/api/src/app.setup.ts b/apps/api/src/app.setup.ts index 495d07f..8d666b9 100644 --- a/apps/api/src/app.setup.ts +++ b/apps/api/src/app.setup.ts @@ -1,4 +1,4 @@ -import { INestApplication, ValidationPipe } from "@nestjs/common"; +import { INestApplication, ValidationPipe } from '@nestjs/common'; export function configureApp(app: INestApplication) { app.useGlobalPipes( diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 9d84dec..ceffb22 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,10 +1,10 @@ -import "dotenv/config"; -import { NestFactory } from "@nestjs/core"; -import { AppModule } from "./app.module"; -import { configureApp } from "./app.setup"; +import 'dotenv/config'; +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; +import { configureApp } from './app.setup'; async function bootstrap() { const app = configureApp(await NestFactory.create(AppModule)); - await app.listen(process.env.PORT ?? 3000, "0.0.0.0"); + await app.listen(process.env.PORT ?? 3000, '0.0.0.0'); } -bootstrap(); \ No newline at end of file +bootstrap(); diff --git a/apps/api/src/supabase/supabase.module.ts b/apps/api/src/supabase/supabase.module.ts index b3fdbea..6aa074d 100644 --- a/apps/api/src/supabase/supabase.module.ts +++ b/apps/api/src/supabase/supabase.module.ts @@ -1,6 +1,6 @@ -import { Global, Module } from "@nestjs/common"; -import { SupabaseService } from "./supabase.service"; +import { Global, Module } from '@nestjs/common'; +import { SupabaseService } from './supabase.service'; @Global() @Module({ providers: [SupabaseService], exports: [SupabaseService] }) -export class SupabaseModule {} \ No newline at end of file +export class SupabaseModule {} diff --git a/apps/api/src/supabase/supabase.service.ts b/apps/api/src/supabase/supabase.service.ts index edf25e6..fbfd78a 100644 --- a/apps/api/src/supabase/supabase.service.ts +++ b/apps/api/src/supabase/supabase.service.ts @@ -1,5 +1,5 @@ -import { Injectable } from "@nestjs/common"; -import { createClient, SupabaseClient } from "@supabase/supabase-js"; +import { Injectable } from '@nestjs/common'; +import { createClient, SupabaseClient } from '@supabase/supabase-js'; function requireEnv(name: string): string { const value = process.env[name]; @@ -11,8 +11,8 @@ function requireEnv(name: string): string { export class SupabaseService { // Service-role client: bypasses RLS. Server-side only. readonly admin: SupabaseClient = createClient( - requireEnv("SUPABASE_URL"), - requireEnv("SUPABASE_SERVICE_ROLE_KEY"), + requireEnv('SUPABASE_URL'), + requireEnv('SUPABASE_SERVICE_ROLE_KEY'), { auth: { persistSession: false, autoRefreshToken: false } }, ); -} \ No newline at end of file +} diff --git a/apps/api/test/setup-env.ts b/apps/api/test/setup-env.ts index fd3e093..14e42dd 100644 --- a/apps/api/test/setup-env.ts +++ b/apps/api/test/setup-env.ts @@ -1,2 +1,2 @@ -import { config } from "dotenv"; -config({ path: ".env" }); \ No newline at end of file +import { config } from 'dotenv'; +config({ path: '.env' }); diff --git a/apps/mobile/__tests__/auth-flow.test.tsx b/apps/mobile/__tests__/auth-flow.test.tsx index 816872d..e76a1e8 100644 --- a/apps/mobile/__tests__/auth-flow.test.tsx +++ b/apps/mobile/__tests__/auth-flow.test.tsx @@ -1,9 +1,4 @@ -import { - fireEvent, - render, - userEvent, - waitFor, -} from "@testing-library/react-native"; +import { fireEvent, render, waitFor } from "@testing-library/react-native"; import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { CreateAccountScreen } from "../features/auth/CreateAccountScreen"; diff --git a/eslint.config.mjs b/eslint.config.mjs index 1cdfa92..5b6cad1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,7 +2,16 @@ import js from "@eslint/js"; import tseslint from "typescript-eslint"; export default [ - { ignores: ["coverage/**", "node_modules/**", "dist/**"] }, + { + ignores: [ + "coverage/**", + "node_modules/**", + "**/dist/**", + "**/node_modules/**", + "**/coverage/**", + "**/.expo/**", + ], + }, { files: ["**/*.cjs"], languageOptions: { globals: { module: "readonly" } }, From 8047d7b50de9e6a5bfbf17391baa0ba27ed196f1 Mon Sep 17 00:00:00 2001 From: InsertUzername Date: Wed, 21 Oct 2026 16:57:21 -0700 Subject: [PATCH 12/12] chore: allowlist gitleaks false positive in api README --- .gitleaksignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitleaksignore diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 0000000..5293f31 --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1 @@ +9d343ec8416f61765d1a0741ee4cb6f394e50243:apps/api/README.md:generic-api-key:5