diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index c02472b..4dba212 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -20,3 +20,5 @@ jobs: run: npm run verify:build - name: Lint run: npm run lint + - name: Test (format-vector conformance) + run: npm test diff --git a/package-lock.json b/package-lock.json index 50e8397..bacb1ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,9 @@ "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", "tslib": "^2.8.1", - "typescript": "^5.8.3" + "tsx": "^4.23.12", + "typescript": "^5.8.3", + "vitest": "^4.1.10" }, "peerDependencies": { "@hookform/resolvers": "^5.2.2", @@ -238,6 +240,448 @@ "node": ">=14.21.3" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@hookform/resolvers": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", @@ -280,10 +724,11 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.29", @@ -327,9 +772,264 @@ "fastq": "^1.6.0" }, "engines": { - "node": ">= 8" + "node": ">= 8" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/plugin-commonjs": { "version": "28.0.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz", @@ -851,6 +1551,13 @@ "react-hook-form": "7.60.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@standard-schema/utils": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", @@ -908,6 +1615,24 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -990,6 +1715,92 @@ "react-dom": ">=18.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", @@ -1017,6 +1828,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -1105,6 +1926,16 @@ } ] }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1209,6 +2040,13 @@ "source-map": "^0.6.1" } }, + "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/css-declaration-sorter": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", @@ -1418,6 +2256,16 @@ "node": ">=6" } }, + "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/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -1488,6 +2336,55 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1509,6 +2406,16 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "dev": true }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -1548,10 +2455,14 @@ } }, "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "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" }, @@ -1854,47 +2765,308 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "engines": { - "node": ">=0.12.0" + "node": ">=0.12.0" + } + }, + "node_modules/is-path-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-3.0.0.tgz", + "integrity": "sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "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==" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-path-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-3.0.0.tgz", - "integrity": "sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-path-inside": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", - "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@types/estree": "*" + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "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==" - }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -1958,12 +3130,13 @@ } }, "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==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/marked": { @@ -2032,9 +3205,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -2043,7 +3216,6 @@ } ], "license": "MIT", - "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -2081,6 +3253,20 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -2173,6 +3359,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2180,9 +3373,9 @@ "dev": true }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -2205,9 +3398,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -2224,9 +3417,8 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2955,6 +4147,39 @@ "node": ">=0.10.0" } }, + "node_modules/rolldown": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.143.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" + } + }, "node_modules/rollup": { "version": "4.61.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.0.tgz", @@ -3146,6 +4371,13 @@ "node": ">=20.0.0" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/slash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", @@ -3178,7 +4410,6 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3210,6 +4441,20 @@ "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", "dev": true }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/string-hash": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", @@ -3308,6 +4553,50 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3326,6 +4615,25 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -3409,6 +4717,246 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yaml": { "version": "1.10.3", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", diff --git a/package.json b/package.json index 4b1d452..7aac0cb 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "./dist/webcomponent.js" ], "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "tsx scripts/check-format-vectors.ts && vitest run", "build": "rollup -c", "verify:build": "node scripts/verify-build.mjs", "dev": "cd dev/vite && npm run dev", @@ -43,9 +43,9 @@ "dev:angular": "cd dev/angular && npm run dev", "dev:angular:setup": "npm run build && cd dev/angular && npm install", "relay": "cd dev/vite && npm run relay", - "code:format": "biome format ./src ./dev", - "code:format:fix": "biome format --write ./src ./dev", - "lint": "biome lint --error-on-warnings ./src ./dev && biome check ./src ./dev", + "code:format": "biome format ./src ./dev ./scripts", + "code:format:fix": "biome format --write ./src ./dev ./scripts", + "lint": "biome lint --error-on-warnings ./src ./dev ./scripts && biome check ./src ./dev ./scripts && tsc --noEmit", "lint:fix": "biome lint --write ./src ./dev && biome check --write ./src ./dev", "publish-release": "npm publish --access=public" }, @@ -79,7 +79,9 @@ "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", "tslib": "^2.8.1", - "typescript": "^5.8.3" + "tsx": "^4.23.12", + "typescript": "^5.8.3", + "vitest": "^4.1.10" }, "overrides": { "@xmldom/xmldom": "0.8.13", diff --git a/scripts/check-format-vectors.ts b/scripts/check-format-vectors.ts new file mode 100644 index 0000000..113fba7 --- /dev/null +++ b/scripts/check-format-vectors.ts @@ -0,0 +1,182 @@ +/** + * Conformance gate for the hub's local FORMAT_PATTERNS copy, run via `npm test`. + * Two offline layers — be precise about what each can and cannot catch: + * + * 1. Vectors: every pattern passes the pinned accept/reject vectors, and every + * registry key has vectors. Catches semantic drift the vectors encode — but NOT + * a regression that keeps the accepted language identical (the round-1 quadratic + * `url` form passed every vector). + * 2. Pinned canonical sources: every pattern's source+flags must equal the + * canonical regex text pinned below. Catches language-identical rewrites + * drifting from canonical. + * + * What NO layer here can catch: a format ADDED to connect's registry after these + * pins were taken. Both sides of every check are hub-local snapshots, and there is + * no live comparison — `StackOneHQ/connect` is private, so an unauthenticated fetch + * of the canonical file 404s permanently, and pulling private source into this + * public repo's CI is not an option. An upstream format addition therefore requires + * a MANUAL sync: bump the registry copy (utils/zodSchema.ts), these vectors, and + * the pinned sources together. + * + * Canonical sources: connect repo, `packages/core/src/connector/formatPatterns.ts` + * and `packages/core/src/connector/specs/formatPatterns.vectors.ts`. + */ +import { FORMAT_PATTERNS } from '../src/modules/integration-picker/utils/zodSchema'; + +// Layer 2 pin: the canonical regex text, verbatim (source without delimiters, then +// flags). Snapshot of connect#1304 head — bump alongside the registry copy. +const CANONICAL_PATTERN_SOURCES: Record = { + email: { source: '^[^\\s@]+@(?=[^\\s@]+\\.[^\\s@])[^\\s@]+$', flags: '' }, + url: { source: '^https?:\\/\\/[^\\s/?#]+(?:[/?#]\\S*)?$', flags: '' }, + uri: { source: '^[a-zA-Z][a-zA-Z0-9+.-]*:\\S+$', flags: '' }, + uuid: { + source: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', + flags: 'i', + }, + date: { source: '^\\d{4}-\\d{2}-\\d{2}$', flags: '' }, + datetime: { + source: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[Zz]|[+-]\\d{2}(?::?\\d{2})?)?$', + flags: '', + }, +}; + +const FORMAT_PATTERN_TEST_VECTORS: Record = { + email: { + accepts: ['john@example.com', 'a.b+tag@sub.domain.co'], + rejects: [ + 'not-an-email', + 'a b@example.com', + '@example.com', + 'john@', + 'john@example.com extra text', + ], + }, + url: { + accepts: [ + 'https://api.example.com', + 'http://x.io/path?q=1', + 'http://localhost:3000', + 'https://1.2.3.4/p?q=1', + 'https://x.io/a?b=c#d', + ], + rejects: [ + 'example.com', + 'ftp://host', + '', + 'https://api.example.com extra text', + 'https://foo bar/baz', + 'https://', + 'https://?q=1', + 'https:///path', + 'https://#frag', + 'https://a b', + ], + }, + uri: { + accepts: ['https://api.example.com', 'mailto:x@y.z', 'urn:isbn:0451450523'], + rejects: [ + 'no-scheme-here', + '://missing', + '', + 'urn:isbn:0451450523 and some trailing prose', + 'mailto:a@b.c <-- paste error', + ], + }, + uuid: { + accepts: ['123e4567-e89b-12d3-a456-426614174000', '123E4567-E89B-12D3-A456-426614174000'], + rejects: [ + '123e4567', + 'zzze4567-e89b-12d3-a456-426614174000', + '', + '123e4567-e89b-12d3-a456-426614174000 extra', + ], + }, + date: { + accepts: ['2026-07-06', '1999-12-31'], + rejects: ['06-07-2026', '2026/07/06', '2026-7-6', '', '2026-07-06 extra'], + }, + datetime: { + accepts: [ + '2026-07-06T10:30:00', + '2026-07-06T10:30:00Z', + '2026-07-06T10:30:00+01:00', + '2026-07-06T10:30:00.123Z', + '2026-07-06T10:30:00z', + '2026-07-06T10:30:00+0100', + '2026-07-06T10:30:00+01', + ], + rejects: [ + '2026-07-06', + '10:30:00', + '', + '2026-07-06T10:30:00banana', + '2026-07-06T10:30:00Zzz', + ], + }, +}; + +let failures = 0; + +// Layer 1: pattern <-> vector conformance. +for (const [format, vectors] of Object.entries(FORMAT_PATTERN_TEST_VECTORS)) { + const pattern = FORMAT_PATTERNS[format as keyof typeof FORMAT_PATTERNS]; + if (!pattern) { + failures++; + console.error(`FAIL: registry is missing format "${format}"`); + continue; + } + for (const value of vectors.accepts) { + if (!pattern.test(value)) { + failures++; + console.error(`FAIL: ${format} should accept "${value}"`); + } + } + for (const value of vectors.rejects) { + if (pattern.test(value)) { + failures++; + console.error(`FAIL: ${format} should reject "${value}"`); + } + } +} + +// Reverse coverage: a format added to the registry (mirroring a new core format) with no +// vectors here would otherwise pass silently — the exact drift D3 exists to catch (a field +// with an unrecognised format renders unvalidated). Fail if any registry key lacks vectors. +for (const format of Object.keys(FORMAT_PATTERNS)) { + if (!FORMAT_PATTERN_TEST_VECTORS[format]) { + failures++; + console.error(`FAIL: no vectors for registry format "${format}"`); + } +} + +// Layer 2: pinned canonical sources — catches a rewrite that keeps the accepted +// language identical (invisible to vectors) but drifts from the canonical regex text. +for (const [format, canonical] of Object.entries(CANONICAL_PATTERN_SOURCES)) { + const pattern = FORMAT_PATTERNS[format as keyof typeof FORMAT_PATTERNS]; + if (!pattern) { + failures++; + console.error(`FAIL: registry is missing pinned canonical format "${format}"`); + continue; + } + if (pattern.source !== canonical.source || pattern.flags !== canonical.flags) { + failures++; + console.error( + `FAIL: ${format} source drifted from the pinned canonical\n local: /${pattern.source}/${pattern.flags}\n canonical: /${canonical.source}/${canonical.flags}`, + ); + } +} +for (const format of Object.keys(FORMAT_PATTERNS)) { + if (!CANONICAL_PATTERN_SOURCES[format]) { + failures++; + console.error(`FAIL: no pinned canonical source for registry format "${format}"`); + } +} + +if (failures > 0) { + console.error(`${failures} format conformance failure(s)`); + process.exit(1); +} + +console.log( + 'All format vectors and canonical-source checks pass (hub-local pins — an upstream format addition needs a manual sync, see header)', +); diff --git a/scripts/verify-build.mjs b/scripts/verify-build.mjs index 162e2fb..e19928b 100644 --- a/scripts/verify-build.mjs +++ b/scripts/verify-build.mjs @@ -1,53 +1,53 @@ -import { createRequire } from "node:module"; -import { readFileSync } from "node:fs"; +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -const { version } = require("../package.json"); +const { version } = require('../package.json'); -const HEADER = "x-hub-version"; -const TOKEN = "__HUB_VERSION__"; +const HEADER = 'x-hub-version'; +const TOKEN = '__HUB_VERSION__'; -const quoted = (literal) => new RegExp(`["']${literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`); +const quoted = (literal) => new RegExp(`["']${literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["']`); const BANNER = /^["']use client["'];/; -const bundles = ["dist/index.esm.js", "dist/index.js", "dist/webcomponent.js"]; -const withBanner = new Set(["dist/index.esm.js", "dist/index.js"]); +const bundles = ['dist/index.esm.js', 'dist/index.js', 'dist/webcomponent.js']; +const withBanner = new Set(['dist/index.esm.js', 'dist/index.js']); const failures = []; for (const file of bundles) { - let source; - - try { - source = readFileSync(new URL(`../${file}`, import.meta.url), "utf8"); - } catch { - failures.push(`${file}: missing — run \`npm run build\` first`); - continue; - } - - if (source.includes(TOKEN)) { - failures.push(`${file}: contains an unreplaced ${TOKEN}`); - } - - if (!quoted(HEADER).test(source)) { - failures.push(`${file}: does not send the ${HEADER} header`); - } - - if (!quoted(version).test(source)) { - failures.push(`${file}: not stamped with version ${version}`); - } - - if (withBanner.has(file) && !BANNER.test(source)) { - failures.push(`${file}: lost its "use client" banner`); - } + let source; + + try { + source = readFileSync(new URL(`../${file}`, import.meta.url), 'utf8'); + } catch { + failures.push(`${file}: missing — run \`npm run build\` first`); + continue; + } + + if (source.includes(TOKEN)) { + failures.push(`${file}: contains an unreplaced ${TOKEN}`); + } + + if (!quoted(HEADER).test(source)) { + failures.push(`${file}: does not send the ${HEADER} header`); + } + + if (!quoted(version).test(source)) { + failures.push(`${file}: not stamped with version ${version}`); + } + + if (withBanner.has(file) && !BANNER.test(source)) { + failures.push(`${file}: lost its "use client" banner`); + } } if (failures.length > 0) { - console.error("Build verification failed:"); - for (const failure of failures) { - console.error(` - ${failure}`); - } - process.exit(1); + console.error('Build verification failed:'); + for (const failure of failures) { + console.error(` - ${failure}`); + } + process.exit(1); } console.log(`Build verified: all bundles stamped with ${version}.`); diff --git a/src/modules/integration-picker/components/IntegrationFields.tsx b/src/modules/integration-picker/components/IntegrationFields.tsx index 6bafaa6..f7561ac 100644 --- a/src/modules/integration-picker/components/IntegrationFields.tsx +++ b/src/modules/integration-picker/components/IntegrationFields.tsx @@ -22,7 +22,7 @@ import useDeepCompareEffect from 'use-deep-compare-effect'; import { AuthenticationNotice, ConnectorConfigField } from '../types'; import { partitionNotices } from '../utils/partitionNotices'; import { formatSecretPlaceholder, isSecretPlaceholder } from '../utils/secretPlaceholder'; -import { createFormSchema } from '../utils/zodSchema'; +import { createFormSchema, createValidationFailureRecorder } from '../utils/zodSchema'; const isInputField = (type: string | undefined): type is 'text' | 'number' | 'password' => { return type === 'text' || type === 'number' || type === 'password'; @@ -225,6 +225,7 @@ interface IntegrationFieldsProps { onChange: (data: Record) => void; onValidationChange?: (isValid: boolean) => void; integrationName: string; + connectorKey?: string; editingSecrets?: Set; setEditingSecrets?: (updater: (prev: Set) => Set) => void; } @@ -278,6 +279,7 @@ export const IntegrationForm: React.FC = ({ error, onValidationChange, integrationName, + connectorKey, editingSecrets, setEditingSecrets, }) => { @@ -286,7 +288,17 @@ export const IntegrationForm: React.FC = ({ typeof f.key === 'object' ? JSON.stringify(f.key) : String(f.key), ); const { noticesBefore, noticesAfter } = partitionNotices(notices, fieldKeys); - const schema = useMemo(() => createFormSchema(fields), [fields]); + + // One recorder for the life of this form session (re-created only when the connector + // changes), NOT per schema build. The schema rebuilds when the connector or account + // data changes (useIntegrationPicker's `fields` memo), so a recorder owned by that memo + // would reset its per-field dedupe on those rebuilds; owning it here keeps the dedupe for + // the whole session. Mirrors the unified-cloud DynamicForm and embedded-widget wiring. + const recordFailure = useMemo( + () => createValidationFailureRecorder(connectorKey), + [connectorKey], + ); + const schema = useMemo(() => createFormSchema(fields, recordFailure), [fields, recordFailure]); const defaultValues = useMemo(() => { const initialData: Record = {}; diff --git a/src/modules/integration-picker/components/IntegrationPickerContent.tsx b/src/modules/integration-picker/components/IntegrationPickerContent.tsx index 91fe11d..280819e 100644 --- a/src/modules/integration-picker/components/IntegrationPickerContent.tsx +++ b/src/modules/integration-picker/components/IntegrationPickerContent.tsx @@ -126,6 +126,7 @@ export const IntegrationPickerContent: React.FC = onChange={onChange} onValidationChange={onValidationChange} integrationName={connectorData.name} + connectorKey={connectorData.key} editingSecrets={editingSecrets} setEditingSecrets={setEditingSecrets} /> diff --git a/src/modules/integration-picker/components/views/IntegrationFormView.tsx b/src/modules/integration-picker/components/views/IntegrationFormView.tsx index 6bf971c..c862851 100644 --- a/src/modules/integration-picker/components/views/IntegrationFormView.tsx +++ b/src/modules/integration-picker/components/views/IntegrationFormView.tsx @@ -12,6 +12,7 @@ interface IntegrationFormViewProps { onChange: (data: Record) => void; onValidationChange?: (isValid: boolean) => void; integrationName: string; + connectorKey?: string; editingSecrets?: Set; setEditingSecrets?: (updater: (prev: Set) => Set) => void; } @@ -23,6 +24,7 @@ export const IntegrationFormView: React.FC = ({ onChange, onValidationChange, integrationName, + connectorKey, editingSecrets, setEditingSecrets, }) => { @@ -34,6 +36,7 @@ export const IntegrationFormView: React.FC = ({ onChange={onChange} onValidationChange={onValidationChange} integrationName={integrationName} + connectorKey={connectorKey} editingSecrets={editingSecrets} setEditingSecrets={setEditingSecrets} /> diff --git a/src/modules/integration-picker/types.ts b/src/modules/integration-picker/types.ts index bf7543c..9a42a3a 100644 --- a/src/modules/integration-picker/types.ts +++ b/src/modules/integration-picker/types.ts @@ -17,6 +17,31 @@ export interface HubData { events_encoded_context?: string; } +// V2/legacy TS connectors — always discriminated by the required `type` on the wire; message field is `error` +export interface LegacyFieldValidation { + type: 'html-pattern' | 'domain'; + pattern: string; + error?: string; + format?: never; + errorMessage?: never; +} + +// Local copy of the format names from `@stackone/core`'s `InputFormat` (connect repo, +// `packages/core/src/connector/types.ts`) — the hub deliberately carries no @stackone +// package dependencies for this feature; keep in sync when a format is added. The +// FORMAT_PATTERNS copy in utils/zodSchema.ts and the vector check in +// scripts/check-format-vectors.ts guard the regexes themselves. +export type FormatName = 'email' | 'url' | 'uuid' | 'date' | 'datetime' | 'uri'; + +// Falcon connectors — no `type`; exactly one of pattern/format is set (XOR), message +// field is `errorMessage`. Local copy of `AuthenticationFieldValidation` from +// `@stackone/core` (connect repo) — keep in sync if the authoring contract changes. +export type FalconFieldValidation = + | { type?: never; error?: never; pattern: string; format?: never; errorMessage?: string } + | { type?: never; error?: never; format: FormatName; pattern?: never; errorMessage?: string }; + +export type FieldValidation = LegacyFieldValidation | FalconFieldValidation; + export interface ConnectorConfigField { type?: 'text' | 'password' | 'number' | 'select' | 'text_area'; label: string; @@ -37,11 +62,13 @@ export interface ConnectorConfigField { }; value?: string | number; condition?: string; - validation?: { - type: 'html-pattern' | 'domain'; - pattern: string; - error?: string; - }; + // Weaker than core's AuthenticationField, which puts `validation?: never` on the + // `select` branch (values already constrained by `options[]`) — this flat copy cannot + // express that, so a select field with `validation` would build a rule here. Exposure + // is nil in practice (0 of 1695 setup/config fields across 503 connectors declare + // `validation` on a select). The build-time rejection of the combination ships in + // connect#1304 (unmerged), not connect `main` yet. + validation?: FieldValidation; display?: boolean; } diff --git a/src/modules/integration-picker/utils/regexSafety.ts b/src/modules/integration-picker/utils/regexSafety.ts new file mode 100644 index 0000000..9da6ca8 --- /dev/null +++ b/src/modules/integration-picker/utils/regexSafety.ts @@ -0,0 +1,115 @@ +/** + * Local copy of the `hasCatastrophicBacktrackingRisk` ReDoS lint from + * `@stackone/connect-sdk` (connect repo, `connect-sdk/src/connectors/regexSafety.ts`) + * — the hub deliberately carries no @stackone package dependencies. Keep in sync with + * the canonical implementation when it changes. + * + * Flags a group repeated two or more times (`*`, `+`, `{n,}`, `{n}`/`{n,m}` with + * max ≥ 2) whose body can match the same text in more than one way: a variable-width + * quantifier inside it (`(a+)+`, `(a{2,4})+`) or an alternation inside it (`(a|a)+`). + * Such patterns backtrack exponentially on crafted near-miss input — and the hub runs + * author patterns on every keystroke. `stackone validate` rejects them at connector + * build time with the canonical lint; this copy re-guards stored patterns from + * connectors built before that gate existed, and the legacy TS path that never had it. + * + * Catches EXPONENTIAL backtracking only. The quadratic "adjacent unbounded quantifier" + * shape (`^a+a+$`) is a deliberate false negative here, as in the canonical lint — some + * live legacy patterns have it. The caller bounds the input length instead + * (MAX_PATTERN_INPUT_LENGTH in zodSchema.ts), the canonical docstring's own recommendation + * for that shape. + */ +export const hasCatastrophicBacktrackingRisk = (source: string): boolean => { + const groups: { hasVariable: boolean; hasAlternation: boolean }[] = []; + let inClass = false; + + // Sticky flag: anchored match at lastIndex without slicing the source per check. + const braceQuantifier = /\{(\d+)(?:(,)(\d*))?\}/y; + + const quantifierAt = ( + index: number, + ): { length: number; variable: boolean; amplifying: boolean } | undefined => { + const char = source[index]; + if (char === '*' || char === '+') { + return { length: 1, variable: true, amplifying: true }; + } + if (char === '?') { + return { length: 1, variable: true, amplifying: false }; + } + if (char === '{') { + braceQuantifier.lastIndex = index; + const match = braceQuantifier.exec(source); + if (match) { + const min = Number(match[1]); + const max = match[2] === undefined ? min : match[3] ? Number(match[3]) : Infinity; + return { length: match[0].length, variable: max > min, amplifying: max >= 2 }; + } + } + return undefined; + }; + + for (let i = 0; i < source.length; i++) { + const char = source[i]; + if (char === '\\') { + i++; + continue; + } + if (inClass) { + if (char === ']') { + inClass = false; + } + continue; + } + if (char === '[') { + inClass = true; + continue; + } + if (char === '|') { + if (groups.length > 0) { + groups[groups.length - 1].hasAlternation = true; + } + continue; + } + if (char === '(') { + groups.push({ hasVariable: false, hasAlternation: false }); + // Skip a group prefix — `(?:`, `(?=`, `(?!`, `(?<=`, `(?`. The + // `?` here opens a special group, it is never a quantifier (nothing precedes + // it to repeat), so it must not mark the group as variable-width. Lookbehind + // (`(?<=` / `(?` body + // is plain chars that fall through harmlessly. + if (source[i + 1] === '?') { + const isLookbehind = + source[i + 2] === '<' && (source[i + 3] === '=' || source[i + 3] === '!'); + i += isLookbehind ? 2 : 1; + } + continue; + } + if (char === ')') { + const closed = groups.pop(); + const quantifier = quantifierAt(i + 1); + if (quantifier) { + if (quantifier.amplifying && (closed?.hasVariable || closed?.hasAlternation)) { + return true; + } + i += quantifier.length; + } + if (groups.length > 0) { + if (quantifier?.variable || closed?.hasVariable) { + groups[groups.length - 1].hasVariable = true; + } + if (closed?.hasAlternation) { + groups[groups.length - 1].hasAlternation = true; + } + } + continue; + } + const quantifier = quantifierAt(i); + if (quantifier) { + if (quantifier.variable && groups.length > 0) { + groups[groups.length - 1].hasVariable = true; + } + i += quantifier.length - 1; + } + } + + return false; +}; diff --git a/src/modules/integration-picker/utils/zodSchema.test.ts b/src/modules/integration-picker/utils/zodSchema.test.ts new file mode 100644 index 0000000..ca5a165 --- /dev/null +++ b/src/modules/integration-picker/utils/zodSchema.test.ts @@ -0,0 +1,317 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { ConnectorConfigField, FieldValidation } from '../types'; +import { createFormSchema, createValidationFailureRecorder } from './zodSchema'; + +// Minimal factory: builds a single-field connector config. Only the properties the +// Zod builder reads (`key`, `label`, `type`, `required`, `validation`) actually matter; +// the rest satisfy the `ConnectorConfigField` shape. +function field(overrides: Partial & { validation?: FieldValidation }) { + return { + key: 'field', + label: 'Field', + type: 'text', + required: false, + readOnly: false, + secret: false, + placeholder: '', + ...overrides, + } satisfies ConnectorConfigField; +} + +describe('createFormSchema — Falcon resolver', () => { + it('accepts an empty value on an OPTIONAL Falcon (pattern) field', () => { + const schema = createFormSchema([ + field({ required: false, validation: { pattern: '^[a-z]+$' } }), + ]); + + const result = schema.safeParse({ field: '' }); + + expect(result.success).toBe(true); + }); + + it('rejects an empty value on a REQUIRED Falcon field with the required message, not the format message', () => { + const schema = createFormSchema([ + field({ label: 'API Key', required: true, validation: { pattern: '^[a-z]+$' } }), + ]); + + const result = schema.safeParse({ field: '' }); + + expect(result.success).toBe(false); + if (!result.success) { + const message = result.error.issues[0].message; + expect(message).toBe('API Key is required'); + expect(message).not.toBe('API Key format is invalid'); + } + }); + + it('rejects a non-empty value violating a pattern with the custom errorMessage when provided', () => { + const schema = createFormSchema([ + field({ + validation: { pattern: '^[a-z]+$', errorMessage: 'Only lowercase letters allowed' }, + }), + ]); + + const result = schema.safeParse({ field: 'ABC123' }); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toBe('Only lowercase letters allowed'); + } + }); + + it('rejects a non-empty pattern violation with the generated fallback "{Label} format is invalid"', () => { + const schema = createFormSchema([ + field({ label: 'Subdomain', validation: { pattern: '^[a-z]+$' } }), + ]); + + const result = schema.safeParse({ field: 'ABC123' }); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toBe('Subdomain format is invalid'); + } + }); + + it('rejects a format: url violation with no errorMessage using "Must be a valid url"', () => { + const schema = createFormSchema([field({ validation: { format: 'url' } })]); + + const result = schema.safeParse({ field: 'not a url' }); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toBe('Must be a valid url'); + } + }); + + it('accepts valid values for format: email, uuid and date', () => { + const emailSchema = createFormSchema([field({ validation: { format: 'email' } })]); + const uuidSchema = createFormSchema([field({ validation: { format: 'uuid' } })]); + const dateSchema = createFormSchema([field({ validation: { format: 'date' } })]); + + expect(emailSchema.safeParse({ field: 'user@example.com' }).success).toBe(true); + expect( + uuidSchema.safeParse({ field: '123e4567-e89b-12d3-a456-426614174000' }).success, + ).toBe(true); + expect(dateSchema.safeParse({ field: '2026-07-29' }).success).toBe(true); + }); +}); + +describe('createFormSchema — Legacy resolver', () => { + it('rejects an invalid value on an OPTIONAL legacy html-pattern field with the error message when provided', () => { + const schema = createFormSchema([ + field({ + required: false, + validation: { type: 'html-pattern', pattern: '^[0-9]+$', error: 'Digits only' }, + }), + ]); + + const result = schema.safeParse({ field: 'abc' }); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toBe('Digits only'); + } + }); + + it('rejects an invalid legacy html-pattern value with the coded fallback when error is omitted', () => { + // NOTE: the code's fallback is `Please match the required format: ${pattern}`, + // NOT the RFC-worded "{Label} is invalid". Asserting the real string. + const schema = createFormSchema([ + field({ + required: false, + validation: { type: 'html-pattern', pattern: '^[0-9]+$' }, + }), + ]); + + const result = schema.safeParse({ field: 'abc' }); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toBe( + 'Please match the required format: ^[0-9]+$', + ); + } + }); + + it('applies the legacy domain ".com" quirk: value must contain "{pattern}.com"', () => { + // resolveLegacyRule wraps a `domain` rule as `.*${pattern}\.com.*`, so the value + // must contain "acme.com" somewhere. A bare "acme" (no ".com") fails; a full + // "https://acme.com/path" passes. + const schema = createFormSchema([ + field({ required: false, validation: { type: 'domain', pattern: 'acme' } }), + ]); + + expect(schema.safeParse({ field: 'acme' }).success).toBe(false); + expect(schema.safeParse({ field: 'https://acme.com/login' }).success).toBe(true); + + const failed = schema.safeParse({ field: 'acme' }); + if (!failed.success) { + expect(failed.error.issues[0].message).toBe('Please enter a valid acme.com domain'); + } + }); + + it('routes on `type:` presence — same value passes as Falcon pattern but fails as legacy', () => { + // Value "acme" satisfies the Falcon pattern ^[a-z]+$ (no `type`), but the legacy + // `domain` rule (has `type`) rewrites the pattern to require ".com", so it fails. + // Demonstrates the discriminated-union routing in isLegacyValidation. + const falconSchema = createFormSchema([field({ validation: { pattern: '^[a-z]+$' } })]); + const legacySchema = createFormSchema([ + field({ validation: { type: 'domain', pattern: 'acme' } }), + ]); + + expect(falconSchema.safeParse({ field: 'acme' }).success).toBe(true); + expect(legacySchema.safeParse({ field: 'acme' }).success).toBe(false); + }); +}); + +describe('createFormSchema — robustness', () => { + it('accepts a saved-secret placeholder on a required field without running the rule', () => { + // Reconnect flow: the field is pre-filled with the redacted sentinel, not a value + // the customer typed. It must not fail validation (which would gate the Connect + // button), even against a strict pattern on a required field. + const schema = createFormSchema([ + field({ required: true, secret: true, validation: { format: 'email' } }), + ]); + + const result = schema.safeParse({ field: '__secretvalue:**redacted**abcd' }); + + expect(result.success).toBe(true); + }); + + it('leaves a field unvalidated (fail-open) when the format is unrecognised', () => { + const schema = createFormSchema([field({ validation: { format: 'hostname' as never } })]); + + expect(schema.safeParse({ field: 'literally anything' }).success).toBe(true); + }); + + it('degrades an uncompilable pattern to no rule instead of throwing during schema build', () => { + // createFormSchema runs inside a render useMemo — an uncompilable pattern that threw + // would take down the whole hub via the error boundary. Treat it as "no rule". + expect(() => createFormSchema([field({ validation: { pattern: '[' } })])).not.toThrow(); + + const schema = createFormSchema([field({ validation: { pattern: '[' } })]); + expect(schema.safeParse({ field: 'anything' }).success).toBe(true); + }); + + it('warns (never fails dark) when a pattern is skipped for ReDoS or compile failure', () => { + // The client is the only enforcement layer, so a silently-dropped rule would render + // a field unvalidated with nobody noticing. Both fail-open paths must warn. + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + createFormSchema([field({ validation: { pattern: '^(a+)+$' } })]); // ReDoS-risky + createFormSchema([field({ validation: { pattern: '[' } })]); // uncompilable + + expect(warn).toHaveBeenCalledTimes(2); + warn.mockRestore(); + }); + + it('does not run a ReDoS-risky author pattern on every keystroke — degrades to no rule', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const schema = createFormSchema([field({ validation: { pattern: '^(a+)+$' } })]); + + expect(schema.safeParse({ field: 'aaaa' }).success).toBe(true); + warn.mockRestore(); + }); + + it('skips an author pattern for a value over the length cap (fail-open), but never caps a format rule', () => { + // Author patterns can backtrack quadratically (the star-height lint misses that + // shape), so a value past the cap is not run. FORMAT_PATTERNS are linear/safe and + // never capped — a long invalid value still fails. + const authorSchema = createFormSchema([field({ validation: { pattern: '^[a-z]+$' } })]); + expect(authorSchema.safeParse({ field: 'A'.repeat(513) }).success).toBe(true); + + const formatSchema = createFormSchema([field({ validation: { format: 'email' } })]); + expect(formatSchema.safeParse({ field: 'x'.repeat(513) }).success).toBe(false); + }); + + it('accepts a saved-secret placeholder on a NUMBER field, so reconnect is not blocked', () => { + // The number branch returns before the string secret-placeholder short-circuit, so + // without its own guard a saved secret would fail `/^\d+$/` and gate Connect. + const schema = createFormSchema([field({ type: 'number', required: true, secret: true })]); + + expect(schema.safeParse({ field: '__secretvalue:**redacted**abcd' }).success).toBe(true); + expect(schema.safeParse({ field: '42' }).success).toBe(true); + expect(schema.safeParse({ field: 'abc' }).success).toBe(false); + }); + + it('accepts the widened datetime offsets synced from @stackone/core', () => { + const schema = createFormSchema([field({ validation: { format: 'datetime' } })]); + + for (const value of [ + '2026-07-06T10:30:00z', + '2026-07-06T10:30:00+0100', + '2026-07-06T10:30:00+01', + ]) { + expect(schema.safeParse({ field: value }).success, value).toBe(true); + } + }); +}); + +describe('createValidationFailureRecorder — lifetime', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('dispatches at most once per field across schema rebuilds when the recorder outlives the schema', () => { + const dispatchEvent = vi.fn(); + vi.stubGlobal('window', { dispatchEvent }); + + // The schema rebuilds when the connector or account data changes (the fields memo), + // so the recorder is owned by the component and passed in. Two builds with failing + // parses simulate two rebuilds; the dedupe must survive them. + const recordFailure = createValidationFailureRecorder('workday'); + const fields = [field({ validation: { pattern: '^[a-z]+$' } })]; + createFormSchema(fields, recordFailure).safeParse({ field: 'BAD1' }); + createFormSchema(fields, recordFailure).safeParse({ field: 'BAD12' }); + + expect(dispatchEvent).toHaveBeenCalledTimes(1); + }); + + it('carries the connector, field and rule kind on the event detail (never the value)', () => { + const dispatchEvent = vi.fn(); + vi.stubGlobal('window', { dispatchEvent }); + + const recordFailure = createValidationFailureRecorder('workday'); + createFormSchema([field({ validation: { pattern: '^[a-z]+$' } })], recordFailure).safeParse( + { field: 'BAD1' }, + ); + + expect(dispatchEvent.mock.calls[0][0].detail).toEqual({ + connector: 'workday', + field: 'field', + ruleKind: 'pattern', + }); + }); +}); + +describe('createFormSchema — ReDoS guard', () => { + it('treats a nested-quantifier pattern as no rule instead of hanging per keystroke', () => { + // `^(a+)+$` backtracks exponentially on a near-miss value; the lint copy in + // regexSafety.ts drops the rule (fail open) so the tab never hangs. + const schema = createFormSchema([field({ validation: { pattern: '^(a+)+$' } })]); + const start = Date.now(); + + const result = schema.safeParse({ field: `${'a'.repeat(60)}!` }); + + expect(result.success).toBe(true); + expect(Date.now() - start).toBeLessThan(1000); + }); + + it('treats a repeated-alternation pattern as no rule (the star-height blind spot)', () => { + const schema = createFormSchema([field({ validation: { pattern: '(a|a)+$' } })]); + const start = Date.now(); + + const result = schema.safeParse({ field: `${'a'.repeat(40)}!` }); + + expect(result.success).toBe(true); + expect(Date.now() - start).toBeLessThan(1000); + }); + + it('still applies a safe legacy html-pattern rule (guard is not over-broad)', () => { + const schema = createFormSchema([ + field({ validation: { type: 'html-pattern', pattern: '^[0-9]+$' } }), + ]); + + expect(schema.safeParse({ field: 'abc' }).success).toBe(false); + }); +}); diff --git a/src/modules/integration-picker/utils/zodSchema.ts b/src/modules/integration-picker/utils/zodSchema.ts index 6e399da..6ed0794 100644 --- a/src/modules/integration-picker/utils/zodSchema.ts +++ b/src/modules/integration-picker/utils/zodSchema.ts @@ -1,7 +1,208 @@ import { z } from 'zod'; -import { ConnectorConfigField } from '../types'; +import { + ConnectorConfigField, + FalconFieldValidation, + FieldValidation, + FormatName, + LegacyFieldValidation, +} from '../types'; +import { hasCatastrophicBacktrackingRisk } from './regexSafety'; +import { isSecretPlaceholder } from './secretPlaceholder'; -function createFieldSchema(field: ConnectorConfigField): z.ZodTypeAny { +// Local copy of the canonical `FORMAT_PATTERNS` registry from `@stackone/core` +// (connect repo, `packages/core/src/connector/formatPatterns.ts`) — the hub +// deliberately carries no @stackone package dependencies for this feature. +// `scripts/check-format-vectors.ts` (run via `npm test`) gates this copy two ways: +// pinned accept/reject vectors, and pinned canonical regex sources (catches +// language-identical rewrites the vectors cannot see). Both pins are hub-local +// snapshots and there is no live comparison (connect is a private repo), so a format +// ADDED upstream is NOT caught automatically — sync this copy, the vectors and the +// pinned sources together, manually, when connect's registry changes. Exported for +// that script. +export const FORMAT_PATTERNS: Record = { + // The lookahead pins the required interior dot without the overlapping + // `[^\s@]+\.[^\s@]+` tail, whose mutual backtracking is quadratic on long + // non-matching input — and this pattern runs on every keystroke. + // Language-identical to the overlapping form (fuzz-verified in @stackone/core). + email: /^[^\s@]+@(?=[^\s@]+\.[^\s@])[^\s@]+$/, + // Host and path/query/fragment split on a disjoint `[/?#]` boundary so the engine + // can never backtrack between them — the overlapping `[^\s/?#]+\S*` form is + // quadratic on long non-matching input, and this pattern runs on every keystroke. + url: /^https?:\/\/[^\s/?#]+(?:[/?#]\S*)?$/, + uri: /^[a-zA-Z][a-zA-Z0-9+.-]*:\S+$/, + uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, + date: /^\d{4}-\d{2}-\d{2}$/, + datetime: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}(?::?\d{2})?)?$/, +}; + +interface ValidationRule { + pattern: RegExp; + errorMessage: string; + // Author-supplied patterns (legacy html-pattern/domain, Falcon pattern) pass the + // ReDoS lint but can still backtrack quadratically on long input (adjacent unbounded + // quantifiers, a shape the star-height lint deliberately misses), so their input is + // length-capped. FORMAT_PATTERNS are canonical + linear and never capped. + capInput?: boolean; +} + +// The star-height lint (regexSafety.ts, canonical connect-sdk copy) catches exponential +// backtracking; it does not catch the quadratic "adjacent unbounded quantifier" shape, +// which some live legacy patterns have. Bound the value fed to an author pattern so a +// pathological one can't hang the tab on crafted long input — the same mitigation the +// canonical lint's own docstring recommends. Auth values (tenant, key, url) are far +// shorter; a value over the cap fails open (skips the rule), matching the degrade elsewhere. +const MAX_PATTERN_INPUT_LENGTH = 512; + +function isLegacyValidation(validation: FieldValidation): validation is LegacyFieldValidation { + return validation.type !== undefined; +} + +// Compile a pattern, degrading to null (no rule) rather than breaking the form, for two +// distinct hazards: an uncompilable pattern would throw inside the render `useMemo` and +// trip the error boundary (whole hub replaced, connector unlinkable), and a +// catastrophic-backtracking pattern would hang the tab at match time — the rule runs on +// every keystroke, and only the compile guard could ever catch the first hazard, not the +// second. connect-sdk rejects both at connector build time, but that gate covers neither +// connectors built before it existed nor the legacy TS path, so re-guard both here. +// Fail open but loudly: a skipped rule degrades to today's no-validation behaviour, and +// each path warns so the field never renders unvalidated silently on the only enforcement +// layer (matches the unknown-format branch below). +function compileRegex(source: string): RegExp | null { + if (hasCatastrophicBacktrackingRisk(source)) { + console.warn( + `[stackone-hub] pattern "${source}" risks catastrophic backtracking — field validation skipped`, + ); + return null; + } + try { + return new RegExp(source); + } catch { + console.warn( + `[stackone-hub] pattern "${source}" failed to compile — field validation skipped`, + ); + return null; + } +} + +// V2/legacy TS connectors — behaviour preserved as-is, delete wholesale when V2 retires +function resolveLegacyRule(validation: LegacyFieldValidation): ValidationRule | null { + if (validation.type === 'html-pattern') { + const pattern = compileRegex(validation.pattern); + if (!pattern) return null; + return { + pattern, + capInput: true, + errorMessage: + validation.error || `Please match the required format: ${validation.pattern}`, + }; + } + + if (validation.type === 'domain') { + const pattern = compileRegex(`.*${validation.pattern}\\.com.*`); + if (!pattern) return null; + return { + pattern, + capInput: true, + errorMessage: + validation.error || `Please enter a valid ${validation.pattern}.com domain`, + }; + } + + return null; +} + +function resolveFalconRule( + validation: FalconFieldValidation, + label: string, +): ValidationRule | null { + if (validation.format) { + const pattern = FORMAT_PATTERNS[validation.format]; + if (!pattern) { + // Unknown format: connect-sdk derives its `format` enum from the canonical + // registry keys, so a format it accepts is missing here — this copy has drifted + // from `@stackone/core`. Fail open (failing closed would lock customers out on a + // hub-version skew) but loudly, since the field then renders unvalidated on the + // only enforcement layer. `scripts/check-format-vectors.ts` should catch this in + // CI; this warns at runtime if a drift ever reaches a customer. + console.warn( + `[stackone-hub] no pattern for format "${validation.format}" — field validation skipped; hub FORMAT_PATTERNS has drifted from @stackone/core`, + ); + return null; + } + return { + pattern, + errorMessage: validation.errorMessage || `Must be a valid ${validation.format}`, + }; + } + + if (validation.pattern) { + const pattern = compileRegex(validation.pattern); + if (!pattern) return null; + return { + pattern, + capInput: true, + errorMessage: validation.errorMessage || `${label} format is invalid`, + }; + } + + return null; +} + +function resolveValidationRule(field: ConnectorConfigField): ValidationRule | null { + if (!field.validation) return null; + + return isLegacyValidation(field.validation) + ? resolveLegacyRule(field.validation) + : resolveFalconRule(field.validation, field.label); +} + +export type RecordValidationFailure = ( + field: ConnectorConfigField, + validation: FieldValidation, +) => void; + +// RFC step 9 (client half): the hub is a customer-embedded package with no analytics +// dependency, so validation failures are surfaced as a DOM CustomEvent — count-only +// (connector key + field key + rule kind, never the value; values may be +// credentials). Hosts or StackOne scripts can listen via +// window.addEventListener('stackone-hub:field-validation-failed', ...). +// +// Lifetime: the recorder must be owned by the rendering component for the life of the +// form session (useMemo keyed on the connector) and passed into createFormSchema — NOT +// created per schema build. The schema is rebuilt whenever the connector or account data +// changes (useIntegrationPicker's `fields` memo, deps [connectorData, selectedIntegration, +// accountData, hubData]), so a recorder owned by the schema would reset its per-field +// dedupe on each of those rebuilds; owning it in the component keeps the dedupe for the +// whole session. No-op outside the browser (e.g. the npm-test vector check). +export function createValidationFailureRecorder(connector?: string): RecordValidationFailure { + const firedFields = new Set(); + + return (field, validation) => { + if (typeof window === 'undefined' || firedFields.has(field.key)) return; + firedFields.add(field.key); + + const format = isLegacyValidation(validation) ? undefined : validation.format; + window.dispatchEvent( + new CustomEvent('stackone-hub:field-validation-failed', { + detail: { + ...(connector ? { connector } : {}), + field: field.key, + ruleKind: isLegacyValidation(validation) + ? 'legacy' + : format + ? 'format' + : 'pattern', + ...(format ? { format } : {}), + }, + }), + ); + }; +} + +function createFieldSchema( + field: ConnectorConfigField, + recordFailure: RecordValidationFailure, +): z.ZodTypeAny { let schema: z.ZodString = z.string(); if (field.required) { @@ -9,39 +210,49 @@ function createFieldSchema(field: ConnectorConfigField): z.ZodTypeAny { } if (field.type === 'number') { + // Per the connector schema, number fields carry no `validation:`, so they never + // reach the rule section below. A saved secret pre-fills as the redacted sentinel + // and must pass on reconnect (same as string fields), so admit it alongside the + // numeric check — otherwise `/^\d+$/` rejects the sentinel and gates Connect. + const isNumericOrSecret = (val: string) => isSecretPlaceholder(val) || /^\d+$/.test(val); if (field.required) { - schema = schema.regex(/^\d+$/, 'Must be a valid number'); - } else { return z .string() - .refine((val) => val === '' || /^\d+$/.test(val), 'Must be a valid number'); + .min(1, `${field.label} is required`) + .refine(isNumericOrSecret, 'Must be a valid number'); } + return z + .string() + .refine((val) => val === '' || isNumericOrSecret(val), 'Must be a valid number'); } - if (field.validation) { - if (field.validation.type === 'html-pattern') { - const pattern = new RegExp(field.validation.pattern); - const errorMessage = - field.validation.error || - `Please match the required format: ${field.validation.pattern}`; - - if (field.required) { - schema = schema.regex(pattern, errorMessage); - } else { - return z.string().refine((val) => val === '' || pattern.test(val), errorMessage); - } - } else if (field.validation.type === 'domain') { - const pattern = new RegExp(`.*${field.validation.pattern}\\.com.*`); - const errorMessage = - field.validation.error || - `Please enter a valid ${field.validation.pattern}.com domain`; - - if (field.required) { - schema = schema.regex(pattern, errorMessage); - } else { - return z.string().refine((val) => val === '' || pattern.test(val), errorMessage); + const validation = field.validation; + const rule = resolveValidationRule(field); + if (rule && validation) { + const testWithMetric = (val: string) => { + // A saved secret is pre-filled as the redacted sentinel (`__secretvalue:**…`), + // not the real value the customer typed. RHF validates `defaultValues` eagerly, + // so without this guard the sentinel would fail the rule before the user touches + // anything — blocking reconnect (gating the Connect button) and emitting a + // failure event for an untouched field. Treat it as valid. + if (isSecretPlaceholder(val)) return true; + // An author pattern over the length cap can't be run safely (see + // MAX_PATTERN_INPUT_LENGTH); skip it (fail-open) rather than risk a hang. + if (rule.capInput && val.length > MAX_PATTERN_INPUT_LENGTH) return true; + // The `&& val` guard is load-bearing: zod 4 accumulates all checks (it does not + // short-circuit on `.min(1)`), so this predicate runs on empty values too. Empty + // is a "required" failure, not a format failure — without `&& val` every + // untouched required field would emit a spurious event. + const ok = rule.pattern.test(val); + if (!ok && val) { + recordFailure(field, validation); } + return ok; + }; + if (field.required) { + return schema.refine((val) => testWithMetric(val), rule.errorMessage); } + return z.string().refine((val) => val === '' || testWithMetric(val), rule.errorMessage); } if (!field.required) { @@ -51,11 +262,20 @@ function createFieldSchema(field: ConnectorConfigField): z.ZodTypeAny { return schema; } -export function createFormSchema(fields: ConnectorConfigField[]) { +// `recordFailure` is owned by the caller and must outlive this schema (see +// createValidationFailureRecorder's lifetime note) — schemas are rebuilt per +// keystroke, so a recorder created here would count keystrokes, not fields. +// Defaults to a no-op for callers without telemetry (tests, the vector check). +const noopRecorder: RecordValidationFailure = () => undefined; + +export function createFormSchema( + fields: ConnectorConfigField[], + recordFailure: RecordValidationFailure = noopRecorder, +) { const schemaShape: Record = {}; for (const field of fields) { - schemaShape[field.key] = createFieldSchema(field); + schemaShape[field.key] = createFieldSchema(field, recordFailure); } return z.object(schemaShape); diff --git a/tsconfig.json b/tsconfig.json index 863a081..5f98412 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "jsxImportSource": "react", "forceConsistentCasingInFileNames": true }, - "include": ["src"], + "include": ["src", "scripts"], "exclude": ["**/*.d.ts"], "files": ["src/index.ts"] }