From 23958e8e2cf91ce6f67b9c57749185fb81e83189 Mon Sep 17 00:00:00 2001 From: Vitalii Litvinchuk Date: Sat, 13 Jun 2026 23:23:50 +0300 Subject: [PATCH] first commit --- .gitignore | 513 +++ apps/sequence-ui/.gitignore | 24 + apps/sequence-ui/README.md | 73 + apps/sequence-ui/eslint.config.js | 22 + apps/sequence-ui/index.html | 13 + apps/sequence-ui/package-lock.json | 2781 +++++++++++++++++ apps/sequence-ui/package.json | 31 + apps/sequence-ui/public/favicon.svg | 1 + apps/sequence-ui/public/icons.svg | 24 + apps/sequence-ui/src/App.css | 184 ++ apps/sequence-ui/src/App.tsx | 70 + apps/sequence-ui/src/Dashboard.tsx | 109 + apps/sequence-ui/src/Models.ts | 25 + apps/sequence-ui/src/api.ts | 72 + apps/sequence-ui/src/assets/hero.png | Bin 0 -> 13057 bytes apps/sequence-ui/src/assets/react.svg | 1 + apps/sequence-ui/src/assets/vite.svg | 1 + apps/sequence-ui/src/index.css | 357 +++ apps/sequence-ui/src/main.tsx | 10 + apps/sequence-ui/src/useTodos.ts | 136 + apps/sequence-ui/tsconfig.app.json | 26 + apps/sequence-ui/tsconfig.json | 10 + apps/sequence-ui/tsconfig.node.json | 24 + apps/sequence-ui/vite.config.ts | 7 + services/csharp/SequenceAuth.slnx | 8 + .../example/Controllers/ApiControllerBase.cs | 17 + .../example/Controllers/AuthController.cs | 24 + .../example/Controllers/SecureController.cs | 12 + .../example/Controllers/TodoController.cs | 39 + services/csharp/example/Domain/Models.cs | 13 + .../csharp/example/Features/Auth/Login.cs | 42 + .../csharp/example/Features/Auth/Logout.cs | 28 + .../Features/Todos/ChangeTodoStatus.cs | 37 + .../example/Features/Todos/CreateTodo.cs | 18 + .../csharp/example/Features/Todos/GetTodos.cs | 24 + .../example/Infrastructure/AppDbContext.cs | 27 + .../KebabCaseParameterTransformer.cs | 21 + .../20260612174016_InitialCreate.Designer.cs | 70 + .../20260612174016_InitialCreate.cs | 57 + .../Migrations/AppDbContextModelSnapshot.cs | 67 + .../csharp/example/Options/RedisOptions.cs | 6 + services/csharp/example/Program.cs | 75 + .../example/Properties/launchSettings.json | 23 + services/csharp/example/RedisSequenceStore.cs | 62 + .../example/SequenceAuth.Example.csproj | 25 + .../csharp/example/SequenceAuth.Example.http | 6 + .../example/appsettings.Development.json | 8 + services/csharp/example/appsettings.json | 14 + services/csharp/lib/ISequenceStore.cs | 8 + services/csharp/lib/Models.cs | 38 + services/csharp/lib/SequenceAuth.Lib.csproj | 23 + services/csharp/lib/SequenceAuthExtensions.cs | 17 + services/csharp/lib/SequenceAuthMiddleware.cs | 109 + services/csharp/lib/SequenceAuthOptions.cs | 11 + services/csharp/lib/SequenceManager.cs | 72 + .../CustomWebApplicationFactory.cs | 20 + .../InMemorySequenceStore.cs | 36 + .../SequenceAuth.Example.Tests.csproj | 27 + .../SequenceAuthE2ETests.cs | 117 + .../SequenceAuth.Lib.Tests.csproj | 27 + .../SequenceManagerTests.cs | 88 + services/docker-compose.yml | 26 + services/k6/load-test.js | 68 + workers/sequence-client/package-lock.json | 27 + workers/sequence-client/package.json | 14 + workers/sequence-client/src/config.ts | 9 + workers/sequence-client/src/decorators.ts | 56 + workers/sequence-client/src/index.ts | 5 + workers/sequence-client/src/models.ts | 19 + workers/sequence-client/src/queue-manager.ts | 47 + workers/sequence-client/src/sequence-store.ts | 26 + workers/sequence-client/tsconfig.json | 15 + 72 files changed, 6142 insertions(+) create mode 100644 .gitignore create mode 100644 apps/sequence-ui/.gitignore create mode 100644 apps/sequence-ui/README.md create mode 100644 apps/sequence-ui/eslint.config.js create mode 100644 apps/sequence-ui/index.html create mode 100644 apps/sequence-ui/package-lock.json create mode 100644 apps/sequence-ui/package.json create mode 100644 apps/sequence-ui/public/favicon.svg create mode 100644 apps/sequence-ui/public/icons.svg create mode 100644 apps/sequence-ui/src/App.css create mode 100644 apps/sequence-ui/src/App.tsx create mode 100644 apps/sequence-ui/src/Dashboard.tsx create mode 100644 apps/sequence-ui/src/Models.ts create mode 100644 apps/sequence-ui/src/api.ts create mode 100644 apps/sequence-ui/src/assets/hero.png create mode 100644 apps/sequence-ui/src/assets/react.svg create mode 100644 apps/sequence-ui/src/assets/vite.svg create mode 100644 apps/sequence-ui/src/index.css create mode 100644 apps/sequence-ui/src/main.tsx create mode 100644 apps/sequence-ui/src/useTodos.ts create mode 100644 apps/sequence-ui/tsconfig.app.json create mode 100644 apps/sequence-ui/tsconfig.json create mode 100644 apps/sequence-ui/tsconfig.node.json create mode 100644 apps/sequence-ui/vite.config.ts create mode 100644 services/csharp/SequenceAuth.slnx create mode 100644 services/csharp/example/Controllers/ApiControllerBase.cs create mode 100644 services/csharp/example/Controllers/AuthController.cs create mode 100644 services/csharp/example/Controllers/SecureController.cs create mode 100644 services/csharp/example/Controllers/TodoController.cs create mode 100644 services/csharp/example/Domain/Models.cs create mode 100644 services/csharp/example/Features/Auth/Login.cs create mode 100644 services/csharp/example/Features/Auth/Logout.cs create mode 100644 services/csharp/example/Features/Todos/ChangeTodoStatus.cs create mode 100644 services/csharp/example/Features/Todos/CreateTodo.cs create mode 100644 services/csharp/example/Features/Todos/GetTodos.cs create mode 100644 services/csharp/example/Infrastructure/AppDbContext.cs create mode 100644 services/csharp/example/Infrastructure/KebabCaseParameterTransformer.cs create mode 100644 services/csharp/example/Migrations/20260612174016_InitialCreate.Designer.cs create mode 100644 services/csharp/example/Migrations/20260612174016_InitialCreate.cs create mode 100644 services/csharp/example/Migrations/AppDbContextModelSnapshot.cs create mode 100644 services/csharp/example/Options/RedisOptions.cs create mode 100644 services/csharp/example/Program.cs create mode 100644 services/csharp/example/Properties/launchSettings.json create mode 100644 services/csharp/example/RedisSequenceStore.cs create mode 100644 services/csharp/example/SequenceAuth.Example.csproj create mode 100644 services/csharp/example/SequenceAuth.Example.http create mode 100644 services/csharp/example/appsettings.Development.json create mode 100644 services/csharp/example/appsettings.json create mode 100644 services/csharp/lib/ISequenceStore.cs create mode 100644 services/csharp/lib/Models.cs create mode 100644 services/csharp/lib/SequenceAuth.Lib.csproj create mode 100644 services/csharp/lib/SequenceAuthExtensions.cs create mode 100644 services/csharp/lib/SequenceAuthMiddleware.cs create mode 100644 services/csharp/lib/SequenceAuthOptions.cs create mode 100644 services/csharp/lib/SequenceManager.cs create mode 100644 services/csharp/tests/SequenceAuth.Example.Tests/CustomWebApplicationFactory.cs create mode 100644 services/csharp/tests/SequenceAuth.Example.Tests/InMemorySequenceStore.cs create mode 100644 services/csharp/tests/SequenceAuth.Example.Tests/SequenceAuth.Example.Tests.csproj create mode 100644 services/csharp/tests/SequenceAuth.Example.Tests/SequenceAuthE2ETests.cs create mode 100644 services/csharp/tests/SequenceAuth.Lib.Tests/SequenceAuth.Lib.Tests.csproj create mode 100644 services/csharp/tests/SequenceAuth.Lib.Tests/SequenceManagerTests.cs create mode 100644 services/docker-compose.yml create mode 100644 services/k6/load-test.js create mode 100644 workers/sequence-client/package-lock.json create mode 100644 workers/sequence-client/package.json create mode 100644 workers/sequence-client/src/config.ts create mode 100644 workers/sequence-client/src/decorators.ts create mode 100644 workers/sequence-client/src/index.ts create mode 100644 workers/sequence-client/src/models.ts create mode 100644 workers/sequence-client/src/queue-manager.ts create mode 100644 workers/sequence-client/src/sequence-store.ts create mode 100644 workers/sequence-client/tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68b3398 --- /dev/null +++ b/.gitignore @@ -0,0 +1,513 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea/ + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp + +# ========================= +# Node.js / TypeScript (Frontend & Client) +# ========================= +node_modules/ +dist/ +dist-ssr/ +build/ +.npm/ + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Environment +.env.local +.env.*.local + +# TypeScript +*.tsbuildinfo + +# Testing +coverage/ + +# Custom overrides +appsettings.Local.json +*.sqlite +*.sqlite3 +*.db diff --git a/apps/sequence-ui/.gitignore b/apps/sequence-ui/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/apps/sequence-ui/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/sequence-ui/README.md b/apps/sequence-ui/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/apps/sequence-ui/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/apps/sequence-ui/eslint.config.js b/apps/sequence-ui/eslint.config.js new file mode 100644 index 0000000..ef614d2 --- /dev/null +++ b/apps/sequence-ui/eslint.config.js @@ -0,0 +1,22 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + }, +]) diff --git a/apps/sequence-ui/index.html b/apps/sequence-ui/index.html new file mode 100644 index 0000000..dd13105 --- /dev/null +++ b/apps/sequence-ui/index.html @@ -0,0 +1,13 @@ + + + + + + + sequence-ui + + +
+ + + diff --git a/apps/sequence-ui/package-lock.json b/apps/sequence-ui/package-lock.json new file mode 100644 index 0000000..c5f75f3 --- /dev/null +++ b/apps/sequence-ui/package-lock.json @@ -0,0 +1,2781 @@ +{ + "name": "sequence-ui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sequence-ui", + "version": "0.0.0", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sequence-client": "file:../../workers/sequence-client" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12" + } + }, + "../../workers/sequence-client": { + "version": "1.0.0", + "devDependencies": { + "typescript": "^5.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "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/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", + "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/type-utils": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.61.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", + "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", + "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.0", + "@typescript-eslint/types": "^8.61.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", + "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", + "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", + "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", + "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", + "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.0", + "@typescript-eslint/tsconfig-utils": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz", + "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", + "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.36", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.36.tgz", + "integrity": "sha512-lVq/Df7LXlO79MVaaUHztSwWiG9oXoWHlgvNS51v8Dpd4+G4/VIy6qYePTw31nAVls33nUtnfezYeLkYAak9dg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/electron-to-chromium": { + "version": "1.5.372", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", + "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz", + "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "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.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "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/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "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/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.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.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/sequence-client": { + "resolved": "../../workers/sequence-client", + "link": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz", + "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.61.0", + "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "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.1.18", + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/apps/sequence-ui/package.json b/apps/sequence-ui/package.json new file mode 100644 index 0000000..f964183 --- /dev/null +++ b/apps/sequence-ui/package.json @@ -0,0 +1,31 @@ +{ + "name": "sequence-ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sequence-client": "file:../../workers/sequence-client" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12" + } +} \ No newline at end of file diff --git a/apps/sequence-ui/public/favicon.svg b/apps/sequence-ui/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/apps/sequence-ui/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/sequence-ui/public/icons.svg b/apps/sequence-ui/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/apps/sequence-ui/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/sequence-ui/src/App.css b/apps/sequence-ui/src/App.css new file mode 100644 index 0000000..f90339d --- /dev/null +++ b/apps/sequence-ui/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/apps/sequence-ui/src/App.tsx b/apps/sequence-ui/src/App.tsx new file mode 100644 index 0000000..778905c --- /dev/null +++ b/apps/sequence-ui/src/App.tsx @@ -0,0 +1,70 @@ +import { useState } from 'react'; +import type { UIState, User, AppStatus } from './Models'; +import { api } from './api'; +import Dashboard from './Dashboard'; +import './index.css'; + +function App() { + const [uiState, setUiState] = useState('Uninitialized'); + const [appStatus, setAppStatus] = useState('Idle'); + const [username, setUsername] = useState(''); + const [user, setUser] = useState(null); + + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + if (!username.trim()) return; + + setAppStatus('Loading'); + try { + const loggedInUser = await api.login(username); + setUser(loggedInUser); + setUiState('Active'); + setAppStatus('Success'); + } catch (err) { + console.error(err); + setAppStatus('Error'); + } + }; + + return ( +
+
+

Sequence Todo

+
+ Status: {uiState} +
+
+ +
+ {uiState === 'Uninitialized' ? ( +
+

Login to Todo App

+

Enter a username to initialize a secure Sequence Token chain.

+
+ setUsername(e.target.value)} + className="text-input" + /> + +
+ {appStatus === 'Error' &&

Failed to login. Ensure backend is running.

} +
+ ) : ( + + )} +
+
+ ); +} + +export default App; diff --git a/apps/sequence-ui/src/Dashboard.tsx b/apps/sequence-ui/src/Dashboard.tsx new file mode 100644 index 0000000..9a6e3c8 --- /dev/null +++ b/apps/sequence-ui/src/Dashboard.tsx @@ -0,0 +1,109 @@ +import { useEffect } from 'react'; +import type { User } from './Models'; +import { useTodos } from './useTodos'; +import { SequenceStore } from 'sequence-client'; + +export default function Dashboard({ user }: { user: User }) { + const { + logs, + todos, + fetchStatus, + createStatus, + newTitle, + setNewTitle, + fetchTodos, + handleCreateTodo, + handleChangeStatus, + handleLogout + } = useTodos(); + + useEffect(() => { + fetchTodos(); + }, [fetchTodos]); + + if (fetchStatus === 'StaleToken') { + return ( +
+
+
⚠️
+

Session Compromised or Stale

+

Your security sequence token is invalid or out of sync. For your protection, your session has been terminated.

+ +
+
+ ); + } + + return ( +
+
+
+

Welcome, {user.username} (Requests remaining: {SequenceStore.get().requestsRemaining ?? '?'})

+ +
+ +
+ setNewTitle(e.target.value)} + placeholder="What needs to be done?" + className="text-input" + /> + +
+ +
+ {fetchStatus === 'Loading' && todos.length === 0 ? ( +

Loading todos...

+ ) : todos.length === 0 ? ( +

No todos yet. Create one!

+ ) : ( + todos.map(todo => ( +
+
+ {todo.title} + {todo.status} +
+
+ {todo.status !== 'Completed' && ( + + )} + {todo.status !== 'InProgress' && todo.status !== 'Completed' && todo.status !== 'Canceled' && ( + + )} + {todo.status !== 'Canceled' && ( + + )} +
+
+ )) + )} +
+
+ +
+

Sequence Chain Activity

+

Every action uses a unique token automatically handled by the QueueManager.

+
+ {logs.length === 0 ? ( +
No activity yet
+ ) : ( + logs.map(log => ( +
+ {log.timestamp} + {log.message} +
+ )) + )} +
+
+
+ ); +} diff --git a/apps/sequence-ui/src/Models.ts b/apps/sequence-ui/src/Models.ts new file mode 100644 index 0000000..8064755 --- /dev/null +++ b/apps/sequence-ui/src/Models.ts @@ -0,0 +1,25 @@ +export type UIState = 'Uninitialized' | 'Active'; +export type LogLevel = 'Info' | 'Success' | 'Error' | 'Loading'; + +export interface LogEntry { + id: number; + timestamp: string; + message: string; + level: LogLevel; +} + +export type TodoStatus = 'Pending' | 'InProgress' | 'Completed' | 'Canceled'; + +export interface TodoItem { + id: string; + userId: string; + title: string; + status: TodoStatus; +} + +export type AppStatus = 'Idle' | 'Loading' | 'Success' | 'Error' | 'StaleToken'; + +export interface User { + id: string; + username: string; +} diff --git a/apps/sequence-ui/src/api.ts b/apps/sequence-ui/src/api.ts new file mode 100644 index 0000000..916ad1b --- /dev/null +++ b/apps/sequence-ui/src/api.ts @@ -0,0 +1,72 @@ +import { SequenceProtected, SequenceStore, SequenceState } from 'sequence-client'; +import type { TodoStatus, User } from './Models'; + +export class ApiClient { + private readonly baseUrl = 'http://localhost:5064'; + + async login(username: string): Promise { + const response = await fetch(`${this.baseUrl}/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ username }) + }); + + if (!response.ok) { + throw new Error('Login failed'); + } + + const data = await response.json(); + const nextSeq = response.headers.get('X-Next-Seq'); + const remainingStr = response.headers.get('X-Requests-Remaining'); + + if (nextSeq) { + SequenceStore.set({ + state: SequenceState.Active, + token: nextSeq, + requestsRemaining: remainingStr ? parseInt(remainingStr, 10) : undefined + }); + } + + return data; + } + + @SequenceProtected() + async logout(init?: RequestInit): Promise { + await fetch(`${this.baseUrl}/auth/logout`, { + ...init, + method: 'POST' + }); + SequenceStore.clear(); + } + + @SequenceProtected() + async getTodos(init?: RequestInit): Promise { + return fetch(`${this.baseUrl}/todo/list`, init); + } + + @SequenceProtected() + async createTodo(title: string, init?: RequestInit): Promise { + const headers = new Headers(init?.headers); + headers.set('Content-Type', 'application/json'); + return fetch(`${this.baseUrl}/todo/create`, { + ...init, + method: 'POST', + headers, + body: JSON.stringify({ title }) + }); + } + + @SequenceProtected() + async changeTodoStatus(id: string, status: TodoStatus, init?: RequestInit): Promise { + const headers = new Headers(init?.headers); + return fetch(`${this.baseUrl}/todo/change-status/${id}?status=${status}`, { + ...init, + method: 'PUT', + headers + }); + } +} + +export const api = new ApiClient(); diff --git a/apps/sequence-ui/src/assets/hero.png b/apps/sequence-ui/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 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 P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 diff --git a/apps/sequence-ui/src/assets/react.svg b/apps/sequence-ui/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/apps/sequence-ui/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/sequence-ui/src/assets/vite.svg b/apps/sequence-ui/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/apps/sequence-ui/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/apps/sequence-ui/src/index.css b/apps/sequence-ui/src/index.css new file mode 100644 index 0000000..251e920 --- /dev/null +++ b/apps/sequence-ui/src/index.css @@ -0,0 +1,357 @@ +:root { + --primary: #6366f1; + --primary-hover: #4f46e5; + --bg-color: #0f172a; + --panel-bg: rgba(30, 41, 59, 0.7); + --border-color: rgba(255, 255, 255, 0.1); + --text-main: #f8fafc; + --text-muted: #94a3b8; + --success: #10b981; + --danger: #ef4444; + --warning: #f59e0b; + --info: #3b82f6; + --font-family: 'Inter', -apple-system, sans-serif; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: var(--font-family); + background: var(--bg-color); + background-image: + radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.15) 0px, transparent 50%), + radial-gradient(at 100% 100%, rgba(16, 185, 129, 0.15) 0px, transparent 50%); + color: var(--text-main); + min-height: 100vh; + -webkit-font-smoothing: antialiased; +} + +.app-container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +.app-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.app-header h1 { + font-size: 1.8rem; + font-weight: 700; + background: linear-gradient(to right, #818cf8, #34d399); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.status-badge { + padding: 0.5rem 1rem; + background: var(--panel-bg); + border: 1px solid var(--border-color); + border-radius: 99px; + font-size: 0.875rem; + backdrop-filter: blur(10px); +} + +/* Auth Panel */ +.init-panel { + max-width: 400px; + margin: 4rem auto; + padding: 2.5rem; + background: var(--panel-bg); + border: 1px solid var(--border-color); + border-radius: 16px; + backdrop-filter: blur(12px); + text-align: center; + box-shadow: 0 20px 40px rgba(0,0,0,0.2); +} + +.init-panel h2 { + margin-bottom: 0.5rem; +} + +.init-panel p { + color: var(--text-muted); + margin-bottom: 2rem; + font-size: 0.9rem; + line-height: 1.5; +} + +.login-form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* Dashboard Grid */ +.dashboard-grid { + display: grid; + grid-template-columns: 1fr 350px; + gap: 2rem; + align-items: start; +} + +/* Panels */ +.todo-panel, .logs-panel { + background: var(--panel-bg); + border: 1px solid var(--border-color); + border-radius: 16px; + padding: 1.5rem; + backdrop-filter: blur(12px); + box-shadow: 0 10px 30px rgba(0,0,0,0.1); +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.create-todo-form { + display: flex; + gap: 1rem; + margin-bottom: 2rem; +} + +/* Inputs & Buttons */ +.text-input { + width: 100%; + padding: 0.75rem 1rem; + background: rgba(15, 23, 42, 0.6); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-main); + font-size: 1rem; + outline: none; + transition: all 0.2s; +} + +.text-input:focus { + border-color: var(--primary); + box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2); +} + +button { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 8px; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background: var(--primary); + color: white; +} + +.btn-primary:hover:not(:disabled) { + background: var(--primary-hover); + transform: translateY(-1px); +} + +.btn-secondary { + background: rgba(255,255,255,0.1); + color: white; +} + +.btn-secondary:hover:not(:disabled) { + background: rgba(255,255,255,0.15); +} + +/* Small Action Buttons */ +.btn-sm { + padding: 0.4rem 0.8rem; + font-size: 0.8rem; + border-radius: 6px; +} +.btn-success { background: rgba(16, 185, 129, 0.2); color: #34d399; } +.btn-success:hover { background: rgba(16, 185, 129, 0.3); } +.btn-info { background: rgba(59, 130, 246, 0.2); color: #60a5fa; } +.btn-info:hover { background: rgba(59, 130, 246, 0.3); } +.btn-danger { background: rgba(239, 68, 68, 0.2); color: #f87171; } +.btn-danger:hover { background: rgba(239, 68, 68, 0.3); } + +/* Todo List */ +.todo-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.todo-card { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.25rem; + background: rgba(15, 23, 42, 0.4); + border: 1px solid var(--border-color); + border-radius: 12px; + transition: transform 0.2s, background 0.2s; +} + +.todo-card:hover { + background: rgba(15, 23, 42, 0.6); + transform: translateX(4px); +} + +.todo-info { + display: flex; + align-items: center; + gap: 1rem; +} + +.todo-title { + font-weight: 500; +} + +.todo-actions { + display: flex; + gap: 0.5rem; +} + +.status-Completed .todo-title { + text-decoration: line-through; + color: var(--text-muted); +} +.status-Canceled .todo-title { + color: var(--text-muted); + opacity: 0.7; +} + +/* Badges */ +.badge { + padding: 0.25rem 0.6rem; + border-radius: 99px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} +.badge-Pending { background: rgba(245, 158, 11, 0.2); color: #fbbf24; } +.badge-InProgress { background: rgba(59, 130, 246, 0.2); color: #60a5fa; } +.badge-Completed { background: rgba(16, 185, 129, 0.2); color: #34d399; } +.badge-Canceled { background: rgba(239, 68, 68, 0.2); color: #f87171; } + +/* Logs Panel */ +.logs-subtitle { + color: var(--text-muted); + font-size: 0.85rem; + margin: 0.5rem 0 1.5rem 0; + line-height: 1.4; +} + +.logs-container { + display: flex; + flex-direction: column; + gap: 0.5rem; + max-height: 500px; + overflow-y: auto; + padding-right: 0.5rem; +} + +.logs-container::-webkit-scrollbar { + width: 6px; +} +.logs-container::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.2); + border-radius: 3px; +} + +.log-entry { + padding: 0.75rem; + background: rgba(15, 23, 42, 0.6); + border-radius: 8px; + font-size: 0.85rem; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + border-left: 3px solid transparent; + animation: slideIn 0.3s ease-out; +} + +@keyframes slideIn { + from { opacity: 0; transform: translateX(10px); } + to { opacity: 1; transform: translateX(0); } +} + +.log-time { + color: var(--text-muted); + margin-right: 0.75rem; +} + +.log-Success { border-left-color: var(--success); } +.log-Error { border-left-color: var(--danger); } +.log-Loading { border-left-color: var(--info); } +.log-Info { border-left-color: var(--text-muted); } + +.error-text { + color: var(--danger); + margin-top: 1rem; + font-size: 0.9rem; +} +.loading-text, .empty-text { + color: var(--text-muted); + text-align: center; + padding: 2rem 0; + font-style: italic; +} + +/* Stale Token Panel */ +.stale-token-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 60vh; +} + +.stale-token-card { + background: var(--panel-bg); + border: 1px solid var(--danger); + border-radius: 16px; + padding: 3rem; + text-align: center; + max-width: 500px; + backdrop-filter: blur(12px); + box-shadow: 0 20px 40px rgba(239, 68, 68, 0.15); + animation: pulse-danger 2s infinite alternate; +} + +.stale-token-card .icon { + font-size: 4rem; + margin-bottom: 1rem; +} + +.stale-token-card h2 { + color: #fca5a5; + margin-bottom: 1rem; +} + +.stale-token-card p { + color: var(--text-muted); + margin-bottom: 2rem; + line-height: 1.5; +} + +.btn-large { + padding: 1rem 2rem; + font-size: 1.1rem; + width: 100%; +} + +@keyframes pulse-danger { + 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); } + 100% { box-shadow: 0 0 0 15px rgba(239, 68, 68, 0); } +} diff --git a/apps/sequence-ui/src/main.tsx b/apps/sequence-ui/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/apps/sequence-ui/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/apps/sequence-ui/src/useTodos.ts b/apps/sequence-ui/src/useTodos.ts new file mode 100644 index 0000000..d01e696 --- /dev/null +++ b/apps/sequence-ui/src/useTodos.ts @@ -0,0 +1,136 @@ +import { useState, useCallback, useRef } from 'react'; +import type { LogEntry, TodoItem, AppStatus, TodoStatus } from './Models'; +import { api } from './api'; +import { SequenceStore } from 'sequence-client'; + +let nextLogId = 1; + +export function useTodos() { + const [logs, setLogs] = useState([]); + const [todos, setTodos] = useState([]); + const [fetchStatus, setFetchStatus] = useState('Idle'); + const [createStatus, setCreateStatus] = useState('Idle'); + const [newTitle, setNewTitle] = useState(''); + const fetchTodosRef = useRef | null>(null); + + const addLog = useCallback((message: string, level: LogEntry['level']) => { + setLogs(prev => { + const newLogs = [{ + id: nextLogId++, + timestamp: new Date().toLocaleTimeString(), + message, + level + }, ...prev]; + return newLogs.slice(0, 50); + }); + }, []); + + const fetchTodos = useCallback(async () => { + if (fetchTodosRef.current) return fetchTodosRef.current; + + fetchTodosRef.current = (async () => { + setFetchStatus('Loading'); + const currentToken = SequenceStore.get().token; + addLog(`GET /todo [Auth-Seq: ${currentToken?.substring(0, 8)}...]`, 'Loading'); + try { + const response = await api.getTodos(); + if (!response.ok) { + if (response.status === 401) throw new Error('StaleToken'); + throw new Error(`HTTP ${response.status}`); + } + const data = await response.json(); + setTodos(data); + setFetchStatus('Success'); + addLog(`GET /todo Success (${data.length} items)`, 'Success'); + } catch (err: any) { + if (err.message === 'StaleToken') { + setFetchStatus('StaleToken'); + } else { + setFetchStatus('Error'); + } + addLog(`GET /todo Error: ${err.message}`, 'Error'); + } + })(); + + try { + await fetchTodosRef.current; + } finally { + fetchTodosRef.current = null; + } + }, [addLog]); + + const handleCreateTodo = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + if (!newTitle.trim() || createStatus === 'Loading') return; + + setCreateStatus('Loading'); + const currentToken = SequenceStore.get().token; + addLog(`POST /todo [Auth-Seq: ${currentToken?.substring(0, 8)}...]`, 'Loading'); + + try { + const response = await api.createTodo(newTitle); + if (!response.ok) { + if (response.status === 401) throw new Error('StaleToken'); + throw new Error(`HTTP ${response.status}`); + } + const newTodo = await response.json(); + setTodos(prev => [...prev, newTodo]); + setNewTitle(''); + setCreateStatus('Success'); + addLog(`POST /todo Success: ${newTodo.title}`, 'Success'); + } catch (err: any) { + if (err.message === 'StaleToken') { + setFetchStatus('StaleToken'); // Fallback to stale state + } else { + setCreateStatus('Error'); + } + addLog(`POST /todo Error: ${err.message}`, 'Error'); + } + }, [newTitle, createStatus, addLog]); + + const handleChangeStatus = useCallback(async (id: string, status: TodoStatus) => { + const currentToken = SequenceStore.get().token; + addLog(`PUT /todo/${id}/status [Auth-Seq: ${currentToken?.substring(0, 8)}...]`, 'Loading'); + + try { + // Optimistic update + setTodos(prev => prev.map(t => t.id === id ? { ...t, status } : t)); + + const response = await api.changeTodoStatus(id, status); + if (!response.ok) { + if (response.status === 401) throw new Error('StaleToken'); + throw new Error(`HTTP ${response.status}`); + } + addLog(`PUT /todo/${id}/status Success`, 'Success'); + } catch (err: any) { + addLog(`PUT /todo/${id}/status Error: ${err.message}`, 'Error'); + if (err.message === 'StaleToken') { + setFetchStatus('StaleToken'); + } else { + fetchTodos(); // Rollback on failure + } + } + }, [addLog, fetchTodos]); + + const handleLogout = useCallback(async () => { + try { + await api.logout(); + } catch (e) { + // ignore + } + window.location.reload(); + }, []); + + return { + logs, + todos, + fetchStatus, + createStatus, + newTitle, + setNewTitle, + fetchTodos, + handleCreateTodo, + handleChangeStatus, + handleLogout + }; +} diff --git a/apps/sequence-ui/tsconfig.app.json b/apps/sequence-ui/tsconfig.app.json new file mode 100644 index 0000000..e549d23 --- /dev/null +++ b/apps/sequence-ui/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "skipLibCheck": true, + "experimentalDecorators": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/apps/sequence-ui/tsconfig.json b/apps/sequence-ui/tsconfig.json new file mode 100644 index 0000000..2784072 --- /dev/null +++ b/apps/sequence-ui/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "experimentalDecorators": true + }, + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/apps/sequence-ui/tsconfig.node.json b/apps/sequence-ui/tsconfig.node.json new file mode 100644 index 0000000..d3c52ea --- /dev/null +++ b/apps/sequence-ui/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/sequence-ui/vite.config.ts b/apps/sequence-ui/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/apps/sequence-ui/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/services/csharp/SequenceAuth.slnx b/services/csharp/SequenceAuth.slnx new file mode 100644 index 0000000..8671a89 --- /dev/null +++ b/services/csharp/SequenceAuth.slnx @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/services/csharp/example/Controllers/ApiControllerBase.cs b/services/csharp/example/Controllers/ApiControllerBase.cs new file mode 100644 index 0000000..20ba194 --- /dev/null +++ b/services/csharp/example/Controllers/ApiControllerBase.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc; + +namespace SequenceAuth.Example.Controllers; + +[ApiController] +[Route("[controller]/[action]")] +public abstract class ApiControllerBase : ControllerBase +{ + protected Guid UserId + { + get + { + var options = HttpContext.RequestServices.GetRequiredService>().Value; + return Guid.Parse(HttpContext.Items[options.UserIdItemKey]?.ToString() ?? throw new UnauthorizedAccessException()); + } + } +} diff --git a/services/csharp/example/Controllers/AuthController.cs b/services/csharp/example/Controllers/AuthController.cs new file mode 100644 index 0000000..41003b5 --- /dev/null +++ b/services/csharp/example/Controllers/AuthController.cs @@ -0,0 +1,24 @@ +using MediatR; +using Microsoft.AspNetCore.Mvc; +using SequenceAuth.Example.Features.Auth; + +namespace SequenceAuth.Example.Controllers; + +public class AuthController(IMediator mediator) : ApiControllerBase +{ + public record LoginRequest(string Username); + + [HttpPost] + public async Task Login([FromBody] LoginRequest request) + { + var result = await mediator.Send(new LoginCommand(request.Username)); + return Ok(result.User); + } + + [HttpPost] + public async Task Logout() + { + await mediator.Send(new LogoutCommand()); + return Ok(); + } +} diff --git a/services/csharp/example/Controllers/SecureController.cs b/services/csharp/example/Controllers/SecureController.cs new file mode 100644 index 0000000..860650e --- /dev/null +++ b/services/csharp/example/Controllers/SecureController.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; + +namespace SequenceAuth.Example.Controllers; + +public class SecureController : ApiControllerBase +{ + [HttpGet] + public IActionResult GetData() + { + return Ok(new { Message = "This is protected data. You must have a valid sequence to see this." }); + } +} diff --git a/services/csharp/example/Controllers/TodoController.cs b/services/csharp/example/Controllers/TodoController.cs new file mode 100644 index 0000000..1a2d686 --- /dev/null +++ b/services/csharp/example/Controllers/TodoController.cs @@ -0,0 +1,39 @@ +using MediatR; +using Microsoft.AspNetCore.Mvc; +using SequenceAuth.Example.Domain; +using SequenceAuth.Example.Features.Todos; + +namespace SequenceAuth.Example.Controllers; + +public class TodoController(IMediator mediator) : ApiControllerBase +{ + public record CreateTodoRequest(string Title); + + [HttpPost] + public async Task Create([FromBody] CreateTodoRequest request) + { + var result = await mediator.Send(new CreateTodoCommand(UserId, request.Title)); + return Ok(result); + } + + [HttpGet] + public async Task List([FromQuery] TodoStatus? status) + { + var result = await mediator.Send(new GetTodosQuery(UserId, status)); + return Ok(result); + } + + [HttpPut("{id}")] + public async Task ChangeStatus(Guid id, [FromQuery] TodoStatus status) + { + var result = await mediator.Send(new ChangeTodoStatusCommand(id, UserId, status)); + + return result.Outcome switch + { + ChangeTodoStatusOutcome.Success => Ok(result.Item), + ChangeTodoStatusOutcome.NotFound => NotFound(), + ChangeTodoStatusOutcome.Unauthorized => StatusCode(403), // No identity claims for Forbid() without auth scheme + _ => StatusCode(500) + }; + } +} diff --git a/services/csharp/example/Domain/Models.cs b/services/csharp/example/Domain/Models.cs new file mode 100644 index 0000000..c6839e2 --- /dev/null +++ b/services/csharp/example/Domain/Models.cs @@ -0,0 +1,13 @@ +namespace SequenceAuth.Example.Domain; + +public record User(Guid Id, string Username); + +public enum TodoStatus +{ + Pending, + InProgress, + Completed, + Canceled +} + +public record TodoItem(Guid Id, Guid UserId, string Title, TodoStatus Status); diff --git a/services/csharp/example/Features/Auth/Login.cs b/services/csharp/example/Features/Auth/Login.cs new file mode 100644 index 0000000..7392ccb --- /dev/null +++ b/services/csharp/example/Features/Auth/Login.cs @@ -0,0 +1,42 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using SequenceAuth.Example.Domain; +using SequenceAuth.Example.Infrastructure; +using SequenceAuth.Lib; + +namespace SequenceAuth.Example.Features.Auth; + +public record LoginCommand(string Username) : IRequest; +public record LoginResult(User User, string InitialSequence); + +public class LoginCommandHandler(AppDbContext dbContext, ISequenceStore sequenceStore, Microsoft.AspNetCore.Http.IHttpContextAccessor httpContextAccessor, Microsoft.Extensions.Options.IOptions options) : IRequestHandler +{ + public async Task Handle(LoginCommand request, CancellationToken cancellationToken) + { + var userOpt = await dbContext.Users.FirstOrDefaultAsync(u => u.Username == request.Username, cancellationToken); + + var user = userOpt switch + { + null => await CreateUser(request.Username, cancellationToken), + _ => userOpt + }; + + var initialSequenceId = Guid.NewGuid().ToString("N"); + var sequenceData = new SequenceData(UserId: user.Id.ToString(), RequestsRemaining: 1000, State: SequenceState.Initialized, NextSequenceId: initialSequenceId); + + await sequenceStore.SaveSequenceAsync(initialSequenceId, sequenceData); + + httpContextAccessor.HttpContext?.Response.Headers.Append(options.Value.NextHeaderName, initialSequenceId); + httpContextAccessor.HttpContext?.Response.Headers.Append(options.Value.RequestsRemainingHeaderName, "1000"); + + return new LoginResult(user, initialSequenceId); + } + + private async Task CreateUser(string username, CancellationToken cancellationToken) + { + var user = new User(Guid.NewGuid(), username); + dbContext.Users.Add(user); + await dbContext.SaveChangesAsync(cancellationToken); + return user; + } +} diff --git a/services/csharp/example/Features/Auth/Logout.cs b/services/csharp/example/Features/Auth/Logout.cs new file mode 100644 index 0000000..37b71b1 --- /dev/null +++ b/services/csharp/example/Features/Auth/Logout.cs @@ -0,0 +1,28 @@ +using MediatR; +using Microsoft.AspNetCore.Http; +using SequenceAuth.Lib; + +namespace SequenceAuth.Example.Features.Auth; + +public record LogoutCommand() : IRequest; + +public class LogoutCommandHandler(ISequenceStore sequenceStore, IHttpContextAccessor httpContextAccessor, Microsoft.Extensions.Options.IOptions options) : IRequestHandler +{ + public async Task Handle(LogoutCommand request, CancellationToken cancellationToken) + { + var userIdStr = httpContextAccessor.HttpContext?.Items[options.Value.UserIdItemKey]?.ToString(); + var isUserIdPresent = string.IsNullOrEmpty(userIdStr); + + _ = isUserIdPresent switch + { + false => await ProcessLogout(userIdStr!), + true => 0 + }; + } + + private async Task ProcessLogout(string userId) + { + await sequenceStore.InvalidateUserSessionsAsync(userId); + return 1; + } +} diff --git a/services/csharp/example/Features/Todos/ChangeTodoStatus.cs b/services/csharp/example/Features/Todos/ChangeTodoStatus.cs new file mode 100644 index 0000000..d56fcee --- /dev/null +++ b/services/csharp/example/Features/Todos/ChangeTodoStatus.cs @@ -0,0 +1,37 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using SequenceAuth.Example.Domain; +using SequenceAuth.Example.Infrastructure; + +namespace SequenceAuth.Example.Features.Todos; + +public enum ChangeTodoStatusOutcome { Success, NotFound, Unauthorized } + +public record ChangeTodoStatusResult(ChangeTodoStatusOutcome Outcome, TodoItem? Item); + +public record ChangeTodoStatusCommand(Guid TodoId, Guid UserId, TodoStatus NewStatus) : IRequest; + +public class ChangeTodoStatusCommandHandler(AppDbContext dbContext) : IRequestHandler +{ + public async Task Handle(ChangeTodoStatusCommand request, CancellationToken cancellationToken) + { + var todo = await dbContext.Todos.FirstOrDefaultAsync(t => t.Id == request.TodoId, cancellationToken); + + return todo switch + { + null => new ChangeTodoStatusResult(ChangeTodoStatusOutcome.NotFound, null), + { UserId: var userId } when userId != request.UserId => new ChangeTodoStatusResult(ChangeTodoStatusOutcome.Unauthorized, null), + _ => await UpdateStatusAsync(todo, request.NewStatus, cancellationToken) + }; + } + + private async Task UpdateStatusAsync(TodoItem todo, TodoStatus newStatus, CancellationToken cancellationToken) + { + var updatedTodo = todo with { Status = newStatus }; + + dbContext.Entry(todo).CurrentValues.SetValues(updatedTodo); + await dbContext.SaveChangesAsync(cancellationToken); + + return new ChangeTodoStatusResult(ChangeTodoStatusOutcome.Success, updatedTodo); + } +} diff --git a/services/csharp/example/Features/Todos/CreateTodo.cs b/services/csharp/example/Features/Todos/CreateTodo.cs new file mode 100644 index 0000000..ab0a9d5 --- /dev/null +++ b/services/csharp/example/Features/Todos/CreateTodo.cs @@ -0,0 +1,18 @@ +using MediatR; +using SequenceAuth.Example.Domain; +using SequenceAuth.Example.Infrastructure; + +namespace SequenceAuth.Example.Features.Todos; + +public record CreateTodoCommand(Guid UserId, string Title) : IRequest; + +public class CreateTodoCommandHandler(AppDbContext dbContext) : IRequestHandler +{ + public async Task Handle(CreateTodoCommand request, CancellationToken cancellationToken) + { + var todo = new TodoItem(Guid.NewGuid(), request.UserId, request.Title, TodoStatus.Pending); + dbContext.Todos.Add(todo); + await dbContext.SaveChangesAsync(cancellationToken); + return todo; + } +} diff --git a/services/csharp/example/Features/Todos/GetTodos.cs b/services/csharp/example/Features/Todos/GetTodos.cs new file mode 100644 index 0000000..a7e53fc --- /dev/null +++ b/services/csharp/example/Features/Todos/GetTodos.cs @@ -0,0 +1,24 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using SequenceAuth.Example.Domain; +using SequenceAuth.Example.Infrastructure; + +namespace SequenceAuth.Example.Features.Todos; + +public record GetTodosQuery(Guid UserId, TodoStatus? StatusFilter) : IRequest>; + +public class GetTodosQueryHandler(AppDbContext dbContext) : IRequestHandler> +{ + public async Task> Handle(GetTodosQuery request, CancellationToken cancellationToken) + { + var query = dbContext.Todos.Where(t => t.UserId == request.UserId); + + query = request.StatusFilter switch + { + null => query, + var status => query.Where(t => t.Status == status) + }; + + return await query.ToListAsync(cancellationToken); + } +} diff --git a/services/csharp/example/Infrastructure/AppDbContext.cs b/services/csharp/example/Infrastructure/AppDbContext.cs new file mode 100644 index 0000000..a4e6299 --- /dev/null +++ b/services/csharp/example/Infrastructure/AppDbContext.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore; +using SequenceAuth.Example.Domain; + +namespace SequenceAuth.Example.Infrastructure; + +public class AppDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Users => Set(); + public DbSet Todos => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Username).IsRequired(); + entity.HasIndex(e => e.Username).IsUnique(); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Title).IsRequired(); + entity.Property(e => e.Status).HasConversion(); + }); + } +} diff --git a/services/csharp/example/Infrastructure/KebabCaseParameterTransformer.cs b/services/csharp/example/Infrastructure/KebabCaseParameterTransformer.cs new file mode 100644 index 0000000..91b166d --- /dev/null +++ b/services/csharp/example/Infrastructure/KebabCaseParameterTransformer.cs @@ -0,0 +1,21 @@ +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Routing; + +namespace SequenceAuth.Example.Infrastructure; + +public class KebabCaseParameterTransformer : IOutboundParameterTransformer +{ + public string? TransformOutbound(object? value) + { + return value == null ? null : value.ToString()!.ToKebabCase(); + } +} + +public static partial class StringExtension +{ + [GeneratedRegex("([a-z0-9])([A-Z])", RegexOptions.Compiled)] + private static partial Regex KebabCaseRule(); + + public static string ToKebabCase(this string input) + => KebabCaseRule().Replace(input, "$1-$2").ToLower().Trim('-'); +} diff --git a/services/csharp/example/Migrations/20260612174016_InitialCreate.Designer.cs b/services/csharp/example/Migrations/20260612174016_InitialCreate.Designer.cs new file mode 100644 index 0000000..53dcc2f --- /dev/null +++ b/services/csharp/example/Migrations/20260612174016_InitialCreate.Designer.cs @@ -0,0 +1,70 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SequenceAuth.Example.Infrastructure; + +#nullable disable + +namespace SequenceAuth.Example.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260612174016_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SequenceAuth.Example.Domain.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Todos"); + }); + + modelBuilder.Entity("SequenceAuth.Example.Domain.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/services/csharp/example/Migrations/20260612174016_InitialCreate.cs b/services/csharp/example/Migrations/20260612174016_InitialCreate.cs new file mode 100644 index 0000000..a3a1f69 --- /dev/null +++ b/services/csharp/example/Migrations/20260612174016_InitialCreate.cs @@ -0,0 +1,57 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SequenceAuth.Example.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Todos", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + Title = table.Column(type: "text", nullable: false), + Status = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Todos", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Username = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_Username", + table: "Users", + column: "Username", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Todos"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/services/csharp/example/Migrations/AppDbContextModelSnapshot.cs b/services/csharp/example/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..ca1da4d --- /dev/null +++ b/services/csharp/example/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,67 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SequenceAuth.Example.Infrastructure; + +#nullable disable + +namespace SequenceAuth.Example.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SequenceAuth.Example.Domain.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Todos"); + }); + + modelBuilder.Entity("SequenceAuth.Example.Domain.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/services/csharp/example/Options/RedisOptions.cs b/services/csharp/example/Options/RedisOptions.cs new file mode 100644 index 0000000..c6b3158 --- /dev/null +++ b/services/csharp/example/Options/RedisOptions.cs @@ -0,0 +1,6 @@ +namespace SequenceAuth.Example.Options; + +public record RedisOptions +{ + public required string Configuration { get; set; } +} \ No newline at end of file diff --git a/services/csharp/example/Program.cs b/services/csharp/example/Program.cs new file mode 100644 index 0000000..5d275a8 --- /dev/null +++ b/services/csharp/example/Program.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Scalar.AspNetCore; +using SequenceAuth.Example.Infrastructure; +using SequenceAuth.Example.Options; +using SequenceAuth.Lib; +using StackExchange.Redis; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(options => +{ + options.Conventions.Add(new RouteTokenTransformerConvention(new KebabCaseParameterTransformer())); +}); + +builder.Services.AddDbContext(options => +{ + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection") ?? "Host=localhost;Database=sequencedb;Username=sequence_user;Password=sequence_password"); +}); + +builder.Services.AddHttpContextAccessor(); +builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly)); + +var sequenceOptions = new SequenceAuthOptions(); +builder.Services.AddSequenceAuth(opts => +{ + opts.IgnoredPaths = sequenceOptions.IgnoredPaths.Concat(["/auth/login"]).ToHashSet(); +}); + +builder.Services.AddOpenApi(); + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader() + .WithExposedHeaders(sequenceOptions.NextHeaderName, sequenceOptions.RequestsRemainingHeaderName); + }); +}); + +builder.Services.Configure(builder.Configuration.GetSection("RedisOptions")); + +builder.Services.AddSingleton(sp => + ConnectionMultiplexer.Connect(sp.GetRequiredService>().Value.Configuration)); + +builder.Services.AddSingleton(); + +var app = builder.Build(); + +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.MigrateAsync(); +} + +app.MapOpenApi(); + +app.MapScalarApiReference(options => +{ + options.Title = "SequenceAuth API"; + options.WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient); +}); + +app.MapGet("/", () => Results.Redirect("/scalar/v1")); + +app.UseCors(); +app.UseMiddleware(); +app.MapControllers(); + +app.Run(); + +public partial class Program { } diff --git a/services/csharp/example/Properties/launchSettings.json b/services/csharp/example/Properties/launchSettings.json new file mode 100644 index 0000000..bd96cd6 --- /dev/null +++ b/services/csharp/example/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5064", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7215;http://localhost:5064", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/services/csharp/example/RedisSequenceStore.cs b/services/csharp/example/RedisSequenceStore.cs new file mode 100644 index 0000000..1a38d4a --- /dev/null +++ b/services/csharp/example/RedisSequenceStore.cs @@ -0,0 +1,62 @@ +using StackExchange.Redis; +using System.Text.Json; +using SequenceAuth.Lib; + +namespace SequenceAuth.Example; + +public class RedisSequenceStore(IConnectionMultiplexer redis) : ISequenceStore +{ + private readonly IDatabase _db = redis.GetDatabase(); + + public async Task> GetSequenceAsync(string sequenceId) + { + var value = await _db.StringGetAsync(sequenceId); + + return ((string?)value) switch + { + null => Option.None(), + string str => ParseData(str) + }; + } + + private static Option ParseData(string str) + { + var data = JsonSerializer.Deserialize(str); + return data switch + { + null => Option.None(), + _ => Option.Some(data) + }; + } + + public async Task SaveSequenceAsync(string sequenceId, SequenceData data) + { + var json = JsonSerializer.Serialize(data); + _ = await _db.StringSetAsync(sequenceId, json); + _ = await _db.SetAddAsync($"UserSessions:{data.UserId}", sequenceId); + return StoreOutcome.Success; + } + + public async Task InvalidateUserSessionsAsync(string userId) + { + var sessionKeys = await _db.SetMembersAsync($"UserSessions:{userId}"); + foreach(var key in sessionKeys) + { + var seqOpt = await GetSequenceAsync(key.ToString()); + _ = seqOpt.State switch + { + OptionState.Some => await UpdateToCompromisedAsync(key.ToString(), seqOpt.Value!), + _ => StoreOutcome.Failure + }; + } + return StoreOutcome.Success; + } + + private async Task UpdateToCompromisedAsync(string sequenceId, SequenceData data) + { + var compData = data with { State = SequenceState.Compromised }; + var json = JsonSerializer.Serialize(compData); + _ = await _db.StringSetAsync(sequenceId, json); + return StoreOutcome.Success; + } +} diff --git a/services/csharp/example/SequenceAuth.Example.csproj b/services/csharp/example/SequenceAuth.Example.csproj new file mode 100644 index 0000000..089330e --- /dev/null +++ b/services/csharp/example/SequenceAuth.Example.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + diff --git a/services/csharp/example/SequenceAuth.Example.http b/services/csharp/example/SequenceAuth.Example.http new file mode 100644 index 0000000..cd8975d --- /dev/null +++ b/services/csharp/example/SequenceAuth.Example.http @@ -0,0 +1,6 @@ +@SequenceAuth.Example_HostAddress = http://localhost:5064 + +GET {{SequenceAuth.Example_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/services/csharp/example/appsettings.Development.json b/services/csharp/example/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/services/csharp/example/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/services/csharp/example/appsettings.json b/services/csharp/example/appsettings.json new file mode 100644 index 0000000..9f34be1 --- /dev/null +++ b/services/csharp/example/appsettings.json @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=sequencedb;Username=sequence_user;Password=sequence_password" + }, + "RedisOptions": { + "Configuration": "localhost:6379" + } +} \ No newline at end of file diff --git a/services/csharp/lib/ISequenceStore.cs b/services/csharp/lib/ISequenceStore.cs new file mode 100644 index 0000000..7f7cb92 --- /dev/null +++ b/services/csharp/lib/ISequenceStore.cs @@ -0,0 +1,8 @@ +namespace SequenceAuth.Lib; + +public interface ISequenceStore +{ + Task> GetSequenceAsync(string sequenceId); + Task SaveSequenceAsync(string sequenceId, SequenceData data); + Task InvalidateUserSessionsAsync(string userId); +} diff --git a/services/csharp/lib/Models.cs b/services/csharp/lib/Models.cs new file mode 100644 index 0000000..be9b039 --- /dev/null +++ b/services/csharp/lib/Models.cs @@ -0,0 +1,38 @@ +namespace SequenceAuth.Lib; + +public enum SequenceState +{ + Initialized, + Active, + Rotated, + Compromised +} + +public enum ValidationOutcome +{ + Success, + SequenceNotFound, + LimitExceeded, + CompromisedSequenceDetected, + InternalError +} + +public enum OptionState +{ + Some, + None +} + +public record Option(T? Value, OptionState State) +{ + public static Option Some(T value) => new(value, OptionState.Some); + public static Option None() => new(default, OptionState.None); +} + +public enum StoreOutcome +{ + Success, + Failure +} + +public record SequenceData(string UserId, int RequestsRemaining, SequenceState State, string NextSequenceId); diff --git a/services/csharp/lib/SequenceAuth.Lib.csproj b/services/csharp/lib/SequenceAuth.Lib.csproj new file mode 100644 index 0000000..7d7d3a7 --- /dev/null +++ b/services/csharp/lib/SequenceAuth.Lib.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + enable + enable + + + SequenceAuth + 1.0.0 + YourName + A C# library for Sequence Token Rotation authorization using Redis. + https://github.com/your-repo/SequenceAuth + authentication;redis;security + true + + + + + + + + diff --git a/services/csharp/lib/SequenceAuthExtensions.cs b/services/csharp/lib/SequenceAuthExtensions.cs new file mode 100644 index 0000000..308e0ed --- /dev/null +++ b/services/csharp/lib/SequenceAuthExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace SequenceAuth.Lib; + +public static class SequenceAuthExtensions +{ + public static IServiceCollection AddSequenceAuth(this IServiceCollection services, Action configureOptions) + { + ArgumentNullException.ThrowIfNull(configureOptions); + + services.Configure(configureOptions); + services.AddScoped(); + + return services; + } +} diff --git a/services/csharp/lib/SequenceAuthMiddleware.cs b/services/csharp/lib/SequenceAuthMiddleware.cs new file mode 100644 index 0000000..e26f830 --- /dev/null +++ b/services/csharp/lib/SequenceAuthMiddleware.cs @@ -0,0 +1,109 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace SequenceAuth.Lib; + +public class SequenceAuthMiddleware(RequestDelegate next, IOptions options) +{ + private readonly SequenceAuthOptions _options = options?.Value ?? throw new InvalidOperationException("SequenceAuthOptions must be registered in DI using AddSequenceAuth."); + + public async Task InvokeAsync(HttpContext context, SequenceManager sequenceManager) + { + await (context.Request.Method switch + { + "OPTIONS" => next(context), + _ => ProcessPath(context, sequenceManager) + }); + } + + private async Task ProcessPath(HttpContext context, SequenceManager sequenceManager) + { + var path = context.Request.Path.Value?.ToLowerInvariant() ?? ""; + + var isIgnored = _options.IgnoredPaths.FirstOrDefault(x => path.StartsWith(x)); + await (isIgnored switch + { + null => CheckHeader(context, sequenceManager), + _ => next(context) + }); + } + + private async Task CheckHeader(HttpContext context, SequenceManager sequenceManager) + { + var headerCount = context.Request.Headers[_options.AuthHeaderName].Count; + + await (headerCount switch + { + 0 => HandleMissingHeader(context), + > 0 => ProcessWithHeader(context, sequenceManager, context.Request.Headers[_options.AuthHeaderName].ToString()!), + _ => HandleMissingHeader(context) + }); + } + + private Task HandleMissingHeader(HttpContext context) + { + context.Response.StatusCode = 401; + return Task.CompletedTask; + } + + private async Task ProcessWithHeader(HttpContext context, SequenceManager sequenceManager, string sequenceId) + { + var result = await sequenceManager.ValidateAndRotateAsync(sequenceId); + + await (result.Outcome switch + { + ValidationOutcome.Success => HandleSuccess(context, result.NextSequence, result.UserId, result.RequestsRemaining), + _ => HandleFailure(context, result.Outcome) + }); + } + + private async Task HandleSuccess(HttpContext context, Option nextSequenceOpt, Option userIdOpt, Option requestsRemainingOpt) + { + _ = nextSequenceOpt.State switch + { + OptionState.Some => AddHeader(context, nextSequenceOpt.Value!), + OptionState.None => 0, + _ => 0 + }; + + _ = userIdOpt.State switch + { + OptionState.Some => AddUserContext(context, userIdOpt.Value!), + OptionState.None => 0, + _ => 0 + }; + + _ = requestsRemainingOpt.State switch + { + OptionState.Some => AddRemainingHeader(context, requestsRemainingOpt.Value), + OptionState.None => 0, + _ => 0 + }; + + await next(context); + } + + private int AddRemainingHeader(HttpContext context, int remaining) + { + context.Response.Headers.Append(_options.RequestsRemainingHeaderName, remaining.ToString()); + return 1; + } + + private int AddHeader(HttpContext context, string value) + { + context.Response.Headers.Append(_options.NextHeaderName, value); + return 1; + } + + private int AddUserContext(HttpContext context, string userId) + { + context.Items[_options.UserIdItemKey] = userId; + return 1; + } + + private Task HandleFailure(HttpContext context, ValidationOutcome outcome) + { + context.Response.StatusCode = 401; + return Task.CompletedTask; + } +} diff --git a/services/csharp/lib/SequenceAuthOptions.cs b/services/csharp/lib/SequenceAuthOptions.cs new file mode 100644 index 0000000..d5cdce5 --- /dev/null +++ b/services/csharp/lib/SequenceAuthOptions.cs @@ -0,0 +1,11 @@ +namespace SequenceAuth.Lib; + +public class SequenceAuthOptions +{ + public virtual string UserIdItemKey { get; set; } = "UserId"; + public virtual string AuthHeaderName { get; set; } = "X-Auth-Seq"; + public virtual string NextHeaderName { get; set; } = "X-Next-Seq"; + public virtual string RequestsRemainingHeaderName { get; set; } = "X-Requests-Remaining"; + + public virtual HashSet IgnoredPaths { get; set; } = ["/scalar", "/openapi", "/favicon.ico", "/swagger"]; +} diff --git a/services/csharp/lib/SequenceManager.cs b/services/csharp/lib/SequenceManager.cs new file mode 100644 index 0000000..c92ba54 --- /dev/null +++ b/services/csharp/lib/SequenceManager.cs @@ -0,0 +1,72 @@ +namespace SequenceAuth.Lib; + +public record SequenceValidationResult(ValidationOutcome Outcome, Option NextSequence, Option UserId, Option RequestsRemaining); + +public class SequenceManager(ISequenceStore store) +{ + public async Task ValidateAndRotateAsync(string currentSequenceId) + { + var sequenceOpt = await store.GetSequenceAsync(currentSequenceId); + + return sequenceOpt.State switch + { + OptionState.None => new SequenceValidationResult(ValidationOutcome.SequenceNotFound, Option.None(), Option.None(), Option.None()), + OptionState.Some => await ProcessSequenceAsync(currentSequenceId, sequenceOpt.Value!), + _ => new SequenceValidationResult(ValidationOutcome.InternalError, Option.None(), Option.None(), Option.None()) + }; + } + + private async Task ProcessSequenceAsync(string currentSequenceId, SequenceData data) + { + return data.State switch + { + SequenceState.Compromised => new SequenceValidationResult(ValidationOutcome.CompromisedSequenceDetected, Option.None(), Option.None(), Option.None()), + SequenceState.Rotated => await HandleRotatedSequenceAsync(data.UserId), + SequenceState.Active or SequenceState.Initialized => await HandleActiveSequenceAsync(currentSequenceId, data), + _ => new SequenceValidationResult(ValidationOutcome.InternalError, Option.None(), Option.None(), Option.None()) + }; + } + + private async Task HandleRotatedSequenceAsync(string userId) + { + await store.InvalidateUserSessionsAsync(userId); + return new SequenceValidationResult(ValidationOutcome.CompromisedSequenceDetected, Option.None(), Option.None(), Option.None()); + } + + private async Task HandleActiveSequenceAsync(string currentSequenceId, SequenceData data) + { + var checkLimit = CheckLimit(data.RequestsRemaining); + return checkLimit switch + { + ValidationOutcome.LimitExceeded => new SequenceValidationResult(ValidationOutcome.LimitExceeded, Option.None(), Option.None(), Option.None()), + ValidationOutcome.Success => await RotateSequenceAsync(currentSequenceId, data), + _ => new SequenceValidationResult(ValidationOutcome.InternalError, Option.None(), Option.None(), Option.None()) + }; + } + + private ValidationOutcome CheckLimit(int requestsRemaining) + { + return requestsRemaining switch + { + <= 0 => ValidationOutcome.LimitExceeded, + > 0 => ValidationOutcome.Success + }; + } + + private async Task RotateSequenceAsync(string currentSequenceId, SequenceData data) + { + var futureSequenceId = Guid.NewGuid().ToString("N"); + var oldDataRotated = data with { State = SequenceState.Rotated }; + await store.SaveSequenceAsync(currentSequenceId, oldDataRotated); + + var newData = new SequenceData( + UserId: data.UserId, + RequestsRemaining: data.RequestsRemaining - 1, + State: SequenceState.Active, + NextSequenceId: futureSequenceId); + + await store.SaveSequenceAsync(futureSequenceId, newData); + + return new SequenceValidationResult(ValidationOutcome.Success, Option.Some(futureSequenceId), Option.Some(data.UserId), Option.Some(data.RequestsRemaining - 1)); + } +} diff --git a/services/csharp/tests/SequenceAuth.Example.Tests/CustomWebApplicationFactory.cs b/services/csharp/tests/SequenceAuth.Example.Tests/CustomWebApplicationFactory.cs new file mode 100644 index 0000000..d4b538e --- /dev/null +++ b/services/csharp/tests/SequenceAuth.Example.Tests/CustomWebApplicationFactory.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using SequenceAuth.Lib; + +namespace SequenceAuth.Example.Tests; + +public class CustomWebApplicationFactory : WebApplicationFactory +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureTestServices(services => + { + services.RemoveAll(); + services.AddSingleton(); + }); + } +} diff --git a/services/csharp/tests/SequenceAuth.Example.Tests/InMemorySequenceStore.cs b/services/csharp/tests/SequenceAuth.Example.Tests/InMemorySequenceStore.cs new file mode 100644 index 0000000..48747d1 --- /dev/null +++ b/services/csharp/tests/SequenceAuth.Example.Tests/InMemorySequenceStore.cs @@ -0,0 +1,36 @@ +using System.Collections.Concurrent; +using System.Linq; +using System.Threading.Tasks; +using SequenceAuth.Lib; + +namespace SequenceAuth.Example.Tests; + +public class InMemorySequenceStore : ISequenceStore +{ + private readonly ConcurrentDictionary _store = new(); + + public Task> GetSequenceAsync(string sequenceId) + { + if (_store.TryGetValue(sequenceId, out var data)) + { + return Task.FromResult(Option.Some(data)); + } + return Task.FromResult(Option.None()); + } + + public Task SaveSequenceAsync(string sequenceId, SequenceData data) + { + _store[sequenceId] = data; + return Task.FromResult(StoreOutcome.Success); + } + + public Task InvalidateUserSessionsAsync(string userId) + { + var keysToRemove = _store.Where(kvp => kvp.Value.UserId == userId).Select(kvp => kvp.Key).ToList(); + foreach (var key in keysToRemove) + { + _store.TryRemove(key, out _); + } + return Task.FromResult(StoreOutcome.Success); + } +} diff --git a/services/csharp/tests/SequenceAuth.Example.Tests/SequenceAuth.Example.Tests.csproj b/services/csharp/tests/SequenceAuth.Example.Tests/SequenceAuth.Example.Tests.csproj new file mode 100644 index 0000000..69caad5 --- /dev/null +++ b/services/csharp/tests/SequenceAuth.Example.Tests/SequenceAuth.Example.Tests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/services/csharp/tests/SequenceAuth.Example.Tests/SequenceAuthE2ETests.cs b/services/csharp/tests/SequenceAuth.Example.Tests/SequenceAuthE2ETests.cs new file mode 100644 index 0000000..a590cdc --- /dev/null +++ b/services/csharp/tests/SequenceAuth.Example.Tests/SequenceAuthE2ETests.cs @@ -0,0 +1,117 @@ +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; +using FluentAssertions; +using SequenceAuth.Lib; +using Xunit; + +namespace SequenceAuth.Example.Tests; + +public class SequenceAuthE2ETests : IClassFixture +{ + private readonly HttpClient _client; + + public SequenceAuthE2ETests(CustomWebApplicationFactory factory) + { + _client = factory.CreateClient(); + } + + private async Task InitSessionAsync() + { + var loginResponse = await _client.PostAsJsonAsync("/auth/login", new { username = "testuser" }); + loginResponse.EnsureSuccessStatusCode(); + + var token = loginResponse.Headers.GetValues(new SequenceAuthOptions().NextHeaderName).FirstOrDefault(); + token.Should().NotBeNullOrWhiteSpace(); + return token!; + } + + [Fact] + public async Task RequestWithoutHeader_ReturnsUnauthorized() + { + var response = await _client.GetAsync("/secure/get-data"); + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task ValidChain_FollowsSequenceAndSucceeds() + { + // 1. Init Session + var token1 = await InitSessionAsync(); + + // 2. First Request + var request1 = new HttpRequestMessage(HttpMethod.Get, "/secure/get-data"); + request1.Headers.Add(new SequenceAuthOptions().AuthHeaderName, token1); + var response1 = await _client.SendAsync(request1); + + response1.EnsureSuccessStatusCode(); + var token2 = response1.Headers.GetValues(new SequenceAuthOptions().NextHeaderName).First(); + token2.Should().NotBeNullOrWhiteSpace(); + + // 3. Second Request with New Token + var request2 = new HttpRequestMessage(HttpMethod.Get, "/secure/get-data"); + request2.Headers.Add(new SequenceAuthOptions().AuthHeaderName, token2); + var response2 = await _client.SendAsync(request2); + + response2.EnsureSuccessStatusCode(); + var token3 = response2.Headers.GetValues(new SequenceAuthOptions().NextHeaderName).First(); + token3.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task AttackerReplaysToken_CompromisesSequence() + { + var token1 = await InitSessionAsync(); + System.Console.WriteLine($"Token 1: {token1}"); + + var userRequest1 = new HttpRequestMessage(HttpMethod.Get, "/secure/get-data"); + userRequest1.Headers.Add(new SequenceAuthOptions().AuthHeaderName, token1); + var userResponse1 = await _client.SendAsync(userRequest1); + userResponse1.EnsureSuccessStatusCode(); + var token2 = userResponse1.Headers.GetValues(new SequenceAuthOptions().NextHeaderName).First(); + System.Console.WriteLine($"Token 2: {token2}"); + + var attackerRequest = new HttpRequestMessage(HttpMethod.Get, "/secure/get-data"); + attackerRequest.Headers.Add(new SequenceAuthOptions().AuthHeaderName, token1); + var attackerResponse = await _client.SendAsync(attackerRequest); + System.Console.WriteLine($"Attacker Response: {attackerResponse.StatusCode}"); + var attackerBody = await attackerResponse.Content.ReadAsStringAsync(); + System.Console.WriteLine($"Attacker Body: {attackerBody}"); + + attackerResponse.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + + var userRequest2 = new HttpRequestMessage(HttpMethod.Get, "/secure/get-data"); + userRequest2.Headers.Add(new SequenceAuthOptions().AuthHeaderName, token2); + var userResponse2 = await _client.SendAsync(userRequest2); + + userResponse2.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task AttackerStrikesFirst_CompromisesSequence() + { + var token1 = await InitSessionAsync(); + + var attackerRequest1 = new HttpRequestMessage(HttpMethod.Get, "/secure/get-data"); + attackerRequest1.Headers.Add(new SequenceAuthOptions().AuthHeaderName, token1); + var attackerResponse1 = await _client.SendAsync(attackerRequest1); + + attackerResponse1.EnsureSuccessStatusCode(); + var token2 = attackerResponse1.Headers.GetValues(new SequenceAuthOptions().NextHeaderName).First(); + + var userRequest1 = new HttpRequestMessage(HttpMethod.Get, "/secure/get-data"); + userRequest1.Headers.Add(new SequenceAuthOptions().AuthHeaderName, token1); + var userResponse1 = await _client.SendAsync(userRequest1); + + System.Console.WriteLine($"User Response 1: {userResponse1.StatusCode}"); + userResponse1.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + + var attackerRequest2 = new HttpRequestMessage(HttpMethod.Get, "/secure/get-data"); + attackerRequest2.Headers.Add(new SequenceAuthOptions().AuthHeaderName, token2); + var attackerResponse2 = await _client.SendAsync(attackerRequest2); + + attackerResponse2.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } +} diff --git a/services/csharp/tests/SequenceAuth.Lib.Tests/SequenceAuth.Lib.Tests.csproj b/services/csharp/tests/SequenceAuth.Lib.Tests/SequenceAuth.Lib.Tests.csproj new file mode 100644 index 0000000..008bc51 --- /dev/null +++ b/services/csharp/tests/SequenceAuth.Lib.Tests/SequenceAuth.Lib.Tests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/services/csharp/tests/SequenceAuth.Lib.Tests/SequenceManagerTests.cs b/services/csharp/tests/SequenceAuth.Lib.Tests/SequenceManagerTests.cs new file mode 100644 index 0000000..891b80d --- /dev/null +++ b/services/csharp/tests/SequenceAuth.Lib.Tests/SequenceManagerTests.cs @@ -0,0 +1,88 @@ +using FluentAssertions; +using Moq; +using SequenceAuth.Lib; +using System.Threading.Tasks; +using Xunit; + +namespace SequenceAuth.Lib.Tests; + +public class SequenceManagerTests +{ + private readonly Mock _mockStore; + private readonly SequenceManager _manager; + + public SequenceManagerTests() + { + _mockStore = new Mock(); + _manager = new SequenceManager(_mockStore.Object); + } + + [Fact] + public async Task ValidateAndRotateAsync_ValidToken_ReturnsSuccessAndGeneratesNewToken() + { + // Arrange + var token = "valid-token"; + var nextToken = "pre-generated-next-token"; + var sequenceData = new SequenceData("user123", 100, SequenceState.Active, nextToken); + + _mockStore.Setup(s => s.GetSequenceAsync(token)) + .ReturnsAsync(Option.Some(sequenceData)); + + _mockStore.Setup(s => s.SaveSequenceAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(StoreOutcome.Success); + + // Act + var result = await _manager.ValidateAndRotateAsync(token); + + // Assert + result.Outcome.Should().Be(ValidationOutcome.Success); + result.NextSequence.State.Should().Be(OptionState.Some); + result.NextSequence.Value.Should().Be(nextToken); + + // Ensure the old sequence was marked as Rotated + _mockStore.Verify(s => s.SaveSequenceAsync(token, It.Is(d => + d.State == SequenceState.Rotated)), Times.Once); + + // Ensure the new sequence was saved + _mockStore.Verify(s => s.SaveSequenceAsync(nextToken, It.Is(d => + d.State == SequenceState.Active)), Times.Once); + } + + [Fact] + public async Task ValidateAndRotateAsync_RotatedToken_ReturnsCompromisedAndDestroysSession() + { + // Arrange + var token = "compromised-token"; + var sequenceData = new SequenceData("user123", 100, SequenceState.Rotated, "some-next-token"); + + _mockStore.Setup(s => s.GetSequenceAsync(token)) + .ReturnsAsync(Option.Some(sequenceData)); + + // Act + var result = await _manager.ValidateAndRotateAsync(token); + + // Assert + result.Outcome.Should().Be(ValidationOutcome.CompromisedSequenceDetected); + result.NextSequence.State.Should().Be(OptionState.None); + + // Ensure InvalidateUserSessionsAsync was called for the user + _mockStore.Verify(s => s.InvalidateUserSessionsAsync("user123"), Times.Once); + } + + [Fact] + public async Task ValidateAndRotateAsync_MissingToken_ReturnsNotFound() + { + // Arrange + var token = "invalid-token"; + + _mockStore.Setup(s => s.GetSequenceAsync(token)) + .ReturnsAsync(Option.None()); + + // Act + var result = await _manager.ValidateAndRotateAsync(token); + + // Assert + result.Outcome.Should().Be(ValidationOutcome.SequenceNotFound); + result.NextSequence.State.Should().Be(OptionState.None); + } +} diff --git a/services/docker-compose.yml b/services/docker-compose.yml new file mode 100644 index 0000000..a1a405d --- /dev/null +++ b/services/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + container_name: sequence-postgres + environment: + POSTGRES_USER: sequence_user + POSTGRES_PASSWORD: sequence_password + POSTGRES_DB: sequencedb + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + redis: + image: redis:7-alpine + container_name: sequence-redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + +volumes: + postgres_data: + redis_data: diff --git a/services/k6/load-test.js b/services/k6/load-test.js new file mode 100644 index 0000000..5fd5835 --- /dev/null +++ b/services/k6/load-test.js @@ -0,0 +1,68 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { Rate } from 'k6/metrics'; + +// Custom metric to explicitly track the percentage of unauthorized requests +const unauthorizedRate = new Rate('unauthorized_rate'); + +export const options = { + // Test scenario: 1000 virtual users executing 1 iteration each + vus: 1000, + iterations: 1000, + // We can also define thresholds for CI/CD pipelines + thresholds: { + 'http_req_duration': ['p(95)<500'], // 95% of requests must complete below 500ms + 'unauthorized_rate': ['rate==0'], // 0% unauthorized errors (strict constraint) + }, +}; + +const BASE_URL = 'http://localhost:5064'; + +export default function () { + // --- Setup / Login Phase --- + // Each VU initializes its own secure session and receives a unique token. + const initRes = http.post(`${BASE_URL}/Init`); + + check(initRes, { + 'init status is 200': (r) => r.status === 200, + 'init returned sequenceId': (r) => r.json('initialSequenceId') !== undefined, + }); + + if (initRes.status !== 200) { + return; // Stop VU if init fails to prevent cascading errors + } + + // Local state for the virtual user + let currentToken = initRes.json('initialSequenceId'); + + // --- Stateful Mutation Phase --- + // The VU performs 50 sequential requests. In a real scenario, these would be GET/POST/PUT. + for (let i = 0; i < 50; i++) { + const res = http.get(`${BASE_URL}/Secure/data`, { + headers: { + 'X-Auth-Seq': currentToken, + }, + }); + + const success = check(res, { + 'status is 200': (r) => r.status === 200, + 'has X-Next-Seq header': (r) => r.headers['X-Next-Seq'] !== undefined, + }); + + if (res.status === 401) { + unauthorizedRate.add(1); + console.error(`VU ${__VU} experienced sequence break on iteration ${i}! Received 401.`); + break; // The chain is broken, stop further requests + } else { + unauthorizedRate.add(0); + } + + if (success) { + // Critical Step: Read X-Next-Seq from Response and save it for the next request in the chain + currentToken = res.headers['X-Next-Seq']; + } + + // Simulate real-world delay between operations (optional, but realistic) + // sleep(Math.random() * 0.1); + } +} diff --git a/workers/sequence-client/package-lock.json b/workers/sequence-client/package-lock.json new file mode 100644 index 0000000..71347c0 --- /dev/null +++ b/workers/sequence-client/package-lock.json @@ -0,0 +1,27 @@ +{ + "name": "sequence-client", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sequence-client", + "version": "1.0.0", + "devDependencies": { + "typescript": "^5.4.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/workers/sequence-client/package.json b/workers/sequence-client/package.json new file mode 100644 index 0000000..5a9cd83 --- /dev/null +++ b/workers/sequence-client/package.json @@ -0,0 +1,14 @@ +{ + "name": "sequence-client", + "version": "1.0.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "tsc --watch" + }, + "dependencies": {}, + "devDependencies": { + "typescript": "^5.4.0" + } +} diff --git a/workers/sequence-client/src/config.ts b/workers/sequence-client/src/config.ts new file mode 100644 index 0000000..43365f9 --- /dev/null +++ b/workers/sequence-client/src/config.ts @@ -0,0 +1,9 @@ +export const SequenceConfig = { + authHeader: 'X-Auth-Seq', + nextHeader: 'X-Next-Seq', + requestsRemainingHeader: 'X-Requests-Remaining' +}; + +export function configureSequenceAuth(config: Partial) { + Object.assign(SequenceConfig, config); +} diff --git a/workers/sequence-client/src/decorators.ts b/workers/sequence-client/src/decorators.ts new file mode 100644 index 0000000..1ffce4a --- /dev/null +++ b/workers/sequence-client/src/decorators.ts @@ -0,0 +1,56 @@ +import { sequenceQueue } from './queue-manager'; +import { SequenceStore } from './sequence-store'; +import { SequenceState } from './models'; +import { SequenceConfig } from './config'; + +export function SequenceProtected(customAuthHeader?: string, customNextHeader?: string) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value; + + descriptor.value = function (...args: any[]) { + return sequenceQueue.enqueue(async () => { + const data = SequenceStore.get(); + + if (data.state === SequenceState.Compromised) { + throw new Error('Session is compromised. Please log in again.'); + } + + const initIndex = Math.max(0, originalMethod.length - 1); + let init: RequestInit = args[initIndex] || {}; + const headers = new Headers(init.headers); + + const authHeader = customAuthHeader || SequenceConfig.authHeader; + const nextHeader = customNextHeader || SequenceConfig.nextHeader; + + if (data.state === SequenceState.Active && data.token) { + headers.set(authHeader, data.token); + } + + init.headers = headers; + args[initIndex] = init; + + // Execute original fetch method + const response: Response = await originalMethod.apply(this, args); + + if (response.status === 401) { + SequenceStore.set({ state: SequenceState.Compromised }); + throw new Error('Sequence validation failed (401). Session compromised.'); + } + + const remainingHeader = SequenceConfig.requestsRemainingHeader; + + const nextSeq = response.headers.get(nextHeader); + const remainingStr = response.headers.get(remainingHeader); + const remaining = remainingStr ? parseInt(remainingStr, 10) : undefined; + + if (nextSeq) { + SequenceStore.set({ state: SequenceState.Active, token: nextSeq, requestsRemaining: remaining }); + } + + return response; + }); + }; + + return descriptor; + }; +} diff --git a/workers/sequence-client/src/index.ts b/workers/sequence-client/src/index.ts new file mode 100644 index 0000000..cad13d0 --- /dev/null +++ b/workers/sequence-client/src/index.ts @@ -0,0 +1,5 @@ +export * from './models'; +export * from './sequence-store'; +export * from './queue-manager'; +export * from './decorators'; +export * from './config'; diff --git a/workers/sequence-client/src/models.ts b/workers/sequence-client/src/models.ts new file mode 100644 index 0000000..6140f5b --- /dev/null +++ b/workers/sequence-client/src/models.ts @@ -0,0 +1,19 @@ +export enum SequenceState { + Empty = 'Empty', + Active = 'Active', + Compromised = 'Compromised' +} + +export enum QueueStatus { + Idle = 'Idle', + Processing = 'Processing', + Paused = 'Paused' +} + +export type FetchStatus = 'Idle' | 'Loading' | 'Success' | 'Error'; + +export interface SequenceData { + state: SequenceState; + token?: string; + requestsRemaining?: number; +} diff --git a/workers/sequence-client/src/queue-manager.ts b/workers/sequence-client/src/queue-manager.ts new file mode 100644 index 0000000..55fcead --- /dev/null +++ b/workers/sequence-client/src/queue-manager.ts @@ -0,0 +1,47 @@ +import { QueueStatus, SequenceState } from './models'; +import { SequenceStore } from './sequence-store'; + +type Task = () => Promise; + +export class QueueManager { + private queue: Array<{ task: Task, resolve: (val: any) => void, reject: (err: any) => void }> = []; + private status: QueueStatus = QueueStatus.Idle; + + async enqueue(task: Task): Promise { + const data = SequenceStore.get(); + + if (data.state === SequenceState.Compromised) { + return Promise.reject(new Error('Sequence state is compromised. Re-authentication required.')); + } + + return new Promise((resolve, reject) => { + this.queue.push({ task, resolve, reject }); + this.processNext(); + }); + } + + private async processNext(): Promise { + if (this.status !== QueueStatus.Idle) { + return; + } + + const nextItem = this.queue.shift(); + if (!nextItem) { + return; + } + + this.status = QueueStatus.Processing; + + try { + const result = await nextItem.task(); + nextItem.resolve(result); + } catch (err) { + nextItem.reject(err); + } finally { + this.status = QueueStatus.Idle; + this.processNext(); + } + } +} + +export const sequenceQueue = new QueueManager(); diff --git a/workers/sequence-client/src/sequence-store.ts b/workers/sequence-client/src/sequence-store.ts new file mode 100644 index 0000000..967ecf5 --- /dev/null +++ b/workers/sequence-client/src/sequence-store.ts @@ -0,0 +1,26 @@ +import { SequenceState, SequenceData } from './models'; + +export class SequenceStore { + private static readonly STORAGE_KEY = 'sequence_token_data'; + + static get(): SequenceData { + const data = localStorage.getItem(this.STORAGE_KEY); + if (!data) { + return { state: SequenceState.Empty }; + } + + try { + return JSON.parse(data) as SequenceData; + } catch { + return { state: SequenceState.Empty }; + } + } + + static set(data: SequenceData): void { + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data)); + } + + static clear(): void { + localStorage.removeItem(this.STORAGE_KEY); + } +} diff --git a/workers/sequence-client/tsconfig.json b/workers/sequence-client/tsconfig.json new file mode 100644 index 0000000..c41f7fe --- /dev/null +++ b/workers/sequence-client/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "experimentalDecorators": true + }, + "include": ["src/**/*"] +}