From 56d9ac63570168d7167ce359a5de4afd5a2b7065 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:39:41 +0000 Subject: [PATCH 01/38] Initial plan From 59c258c0cc9475924353ffcb34d0ee22cfe505e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:55:05 +0000 Subject: [PATCH 02/38] Add message search frontend and backend API Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/782fbe79-d8e5-4b08-9854-1e412d5c534f --- .gitignore | 1 + eslint.config.js | 9 +- package-lock.json | 3331 ++++++++++++++--- package.json | 2 + packages/backend/package.json | 2 + packages/backend/src/index.ts | 4 + .../backend/src/search/search.controller.ts | 31 + .../src/search/search.persistence.service.ts | 63 + packages/frontend/.env.example | 1 + packages/frontend/index.html | 13 + packages/frontend/package.json | 38 +- packages/frontend/postcss.config.js | 6 + packages/frontend/src/App.tsx | 192 + packages/frontend/src/components/ui/badge.tsx | 36 + .../frontend/src/components/ui/button.tsx | 50 + packages/frontend/src/components/ui/card.tsx | 56 + packages/frontend/src/components/ui/input.tsx | 19 + packages/frontend/src/components/ui/label.tsx | 19 + .../frontend/src/components/ui/separator.tsx | 26 + packages/frontend/src/components/ui/table.tsx | 70 + packages/frontend/src/index.css | 69 + packages/frontend/src/lib/utils.ts | 6 + packages/frontend/src/main.tsx | 10 + packages/frontend/src/vite-env.d.ts | 1 + packages/frontend/tailwind.config.js | 57 + packages/frontend/tsconfig.json | 26 + packages/frontend/tsconfig.node.json | 19 + packages/frontend/vite.config.ts | 12 + 28 files changed, 3694 insertions(+), 475 deletions(-) create mode 100644 packages/backend/src/search/search.controller.ts create mode 100644 packages/backend/src/search/search.persistence.service.ts create mode 100644 packages/frontend/.env.example create mode 100644 packages/frontend/index.html create mode 100644 packages/frontend/postcss.config.js create mode 100644 packages/frontend/src/App.tsx create mode 100644 packages/frontend/src/components/ui/badge.tsx create mode 100644 packages/frontend/src/components/ui/button.tsx create mode 100644 packages/frontend/src/components/ui/card.tsx create mode 100644 packages/frontend/src/components/ui/input.tsx create mode 100644 packages/frontend/src/components/ui/label.tsx create mode 100644 packages/frontend/src/components/ui/separator.tsx create mode 100644 packages/frontend/src/components/ui/table.tsx create mode 100644 packages/frontend/src/index.css create mode 100644 packages/frontend/src/lib/utils.ts create mode 100644 packages/frontend/src/main.tsx create mode 100644 packages/frontend/src/vite-env.d.ts create mode 100644 packages/frontend/tailwind.config.js create mode 100644 packages/frontend/tsconfig.json create mode 100644 packages/frontend/tsconfig.node.json create mode 100644 packages/frontend/vite.config.ts diff --git a/.gitignore b/.gitignore index 1639af65..2a1be96f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules dist +*.tsbuildinfo .env *.log coverage diff --git a/eslint.config.js b/eslint.config.js index 6f82ce4d..eb7685f6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,7 +5,14 @@ const tseslint = require('typescript-eslint'); module.exports = tseslint.config( { - ignores: ['**/node_modules/**', '**/dist/**', '**/coverage/**', '**/*.d.ts', 'packages/backend/scripts/*.js'], + ignores: [ + '**/node_modules/**', + '**/dist/**', + '**/coverage/**', + '**/*.d.ts', + 'packages/backend/scripts/*.js', + 'packages/frontend/**', + ], }, js.configs.recommended, ...tseslint.configs.recommended, diff --git a/package-lock.json b/package-lock.json index d1939398..7350035e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,18 @@ "typescript-eslint": "^8.57.1" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/code-frame": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", @@ -486,6 +498,38 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -942,6 +986,23 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", @@ -1080,6 +1141,108 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/js": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", @@ -1760,7 +1923,6 @@ "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", @@ -1782,7 +1944,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1792,14 +1953,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "devOptional": 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", @@ -1827,6 +1986,41 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@paralleldrive/cuid2": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", @@ -1911,95 +2105,554 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@slack/logger": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.1.tgz", - "integrity": "sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==", + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", "license": "MIT", "dependencies": { - "@types/node": ">=18" + "@radix-ui/react-primitive": "2.1.4" }, - "engines": { - "node": ">= 18", - "npm": ">= 8.6.0" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@slack/types": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.20.1.tgz", - "integrity": "sha512-eWX2mdt1ktpn8+40iiMc404uGrih+2fxiky3zBcPjtXKj6HLRdYlmhrPkJi7JTJm8dpXR6BWVWEDBXtaWMKD6A==", + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", "license": "MIT", - "engines": { - "node": ">= 12.13.0", - "npm": ">= 6.12.0" + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@slack/web-api": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.15.0.tgz", - "integrity": "sha512-va7zYIt3QHG1x9M/jqXXRPFMoOVlVSSRHC5YH+DzKYsrz5xUKOA3lR4THsu/Zxha9N1jOndbKFKLtr0WOPW1Vw==", + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", "license": "MIT", "dependencies": { - "@slack/logger": "^4.0.1", - "@slack/types": "^2.20.1", - "@types/node": ">=18", - "@types/retry": "0.12.0", - "axios": "^1.13.5", - "eventemitter3": "^5.0.1", - "form-data": "^4.0.4", - "is-electron": "2.2.2", - "is-stream": "^2", - "p-queue": "^6", - "p-retry": "^4", - "retry": "^0.13.1" + "@radix-ui/react-primitive": "2.1.4" }, - "engines": { - "node": ">= 18", - "npm": ">= 8.6.0" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@slack/web-api/node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@slack/web-api/node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@slack/logger": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.1.tgz", + "integrity": "sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==", + "license": "MIT", + "dependencies": { + "@types/node": ">=18" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@slack/types": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.20.1.tgz", + "integrity": "sha512-eWX2mdt1ktpn8+40iiMc404uGrih+2fxiky3zBcPjtXKj6HLRdYlmhrPkJi7JTJm8dpXR6BWVWEDBXtaWMKD6A==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0", + "npm": ">= 6.12.0" + } + }, + "node_modules/@slack/web-api": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.15.0.tgz", + "integrity": "sha512-va7zYIt3QHG1x9M/jqXXRPFMoOVlVSSRHC5YH+DzKYsrz5xUKOA3lR4THsu/Zxha9N1jOndbKFKLtr0WOPW1Vw==", + "license": "MIT", + "dependencies": { + "@slack/logger": "^4.0.1", + "@slack/types": "^2.20.1", + "@types/node": ">=18", + "@types/retry": "0.12.0", + "axios": "^1.13.5", + "eventemitter3": "^5.0.1", + "form-data": "^4.0.4", + "is-electron": "2.2.2", + "is-stream": "^2", + "p-queue": "^6", + "p-retry": "^4", + "retry": "^0.13.1" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@slack/web-api/node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/@slack/web-api/node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -2151,6 +2804,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/easy-table": { "version": "0.0.32", "resolved": "https://registry.npmjs.org/@types/easy-table/-/easy-table-0.0.32.tgz", @@ -2329,6 +2992,26 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "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==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -2669,6 +3352,27 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -2874,11 +3578,16 @@ "node": ">=14" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -2904,6 +3613,13 @@ "devOptional": true, "license": "MIT" }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -3005,6 +3721,43 @@ "node": ">= 4.5.0" } }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3490,7 +4243,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -3709,10 +4461,19 @@ "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { - "version": "1.0.30001764", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", - "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", "dev": true, "funding": [ { @@ -4050,6 +4811,18 @@ "node": ">= 0.4" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/cli-boxes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", @@ -4152,9 +4925,18 @@ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "license": "MIT", - "optional": true, + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { - "node": ">=0.8" + "node": ">=6" } }, "node_modules/cluster-key-slot": { @@ -4426,6 +5208,23 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/create-error-class": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", @@ -4492,6 +5291,25 @@ "node": ">=4" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -4669,6 +5487,12 @@ "wrappy": "1" } }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -4689,6 +5513,12 @@ "node": ">= 6" } }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, "node_modules/dot-prop": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", @@ -5033,6 +5863,16 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, "node_modules/eslint-scope": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", @@ -5459,6 +6299,34 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "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", @@ -5480,6 +6348,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -5544,7 +6421,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -5793,6 +6669,20 @@ "node": ">= 0.6" } }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -5826,7 +6716,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -6040,7 +6929,6 @@ "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" @@ -6476,6 +7364,23 @@ "dev": true, "license": "ISC" }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/import-lazy": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", @@ -6630,7 +7535,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -6692,7 +7596,6 @@ "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" @@ -6728,7 +7631,6 @@ "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" @@ -6778,7 +7680,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -8422,6 +9323,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8429,6 +9339,19 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -8586,11 +9509,22 @@ "node": ">= 0.8.0" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/lint-staged": { @@ -8683,6 +9617,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -8805,6 +9746,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.511.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.511.0.tgz", + "integrity": "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -8895,6 +9845,15 @@ "dev": true, "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -8908,7 +9867,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -9171,6 +10129,17 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nan": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz", @@ -9179,6 +10148,24 @@ "license": "MIT", "optional": true }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -9385,7 +10372,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9414,6 +10400,15 @@ "node": ">=4" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -9469,6 +10464,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -9761,6 +10765,19 @@ "semver": "bin/semver" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -9827,7 +10844,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -9862,14 +10878,12 @@ "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": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -9892,7 +10906,6 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -9945,46 +10958,202 @@ "dev": true, "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" }, "engines": { - "node": ">=6" + "node": ">= 18" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "postcss-selector-parser": "^6.1.1" }, "engines": { - "node": ">=8" + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" } }, - "node_modules/posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", - "dev": true, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -10184,6 +11353,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -10234,6 +11423,27 @@ "node": ">=0.10.0" } }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -10241,6 +11451,34 @@ "dev": true, "license": "MIT" }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -10521,7 +11759,6 @@ "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -10561,6 +11798,16 @@ "node": ">=8" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -10628,6 +11875,16 @@ "node": ">= 4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -10635,6 +11892,74 @@ "dev": true, "license": "MIT" }, + "node_modules/rollup": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -10688,6 +12013,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -11177,6 +12508,15 @@ "node": ">=0.10.0" } }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", @@ -11540,6 +12880,37 @@ "boundary": "^2.0.0" } }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/superagent": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", @@ -11616,7 +12987,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11625,6 +12995,140 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tailwindcss/node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/tailwindcss/node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/term-size": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", @@ -11766,18 +13270,39 @@ "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" }, "engines": { - "node": "*" + "node": ">=0.8" } }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT" - }, "node_modules/timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", @@ -11802,7 +13327,6 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -11819,7 +13343,6 @@ "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" @@ -11837,7 +13360,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -11919,7 +13441,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -11975,6 +13496,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, "node_modules/ts-jest": { "version": "29.4.6", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", @@ -12237,509 +13764,1082 @@ } } }, - "node_modules/typeorm/node_modules/dedent": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", - "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "node_modules/typeorm/node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typeorm/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", + "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.1", + "@typescript-eslint/parser": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/union-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha512-ODgiYu03y5g76A1I9Gt0/chLCzQjvzDy7DsZGsLOE/1MrF6wriEskSncj1+/C58Xk/kPZDppSctDybCwOSaGAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unzip-response": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha512-N0XH6lqDtFH84JxptQoZYmloF4nzrQqqrAymNj+/gW60AO2AZgOcf4O/nUXJcYfyQkqvMo9lSupBZmmgvuVXlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } + "engines": { + "node": ">=4", + "yarn": "*" } }, - "node_modules/typeorm/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "license": "ISC", + "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": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { - "glob": "dist/esm/bin.mjs" + "update-browserslist-db": "cli.js" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "node_modules/typeorm/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", + "node_modules/update-notifier": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", + "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "brace-expansion": "^2.0.1" + "boxen": "^1.2.1", + "chalk": "^2.0.1", + "configstore": "^3.0.0", + "import-lazy": "^2.1.0", + "is-ci": "^1.0.10", + "is-installed-globally": "^0.1.0", + "is-npm": "^1.0.0", + "latest-version": "^3.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=4" } }, - "node_modules/typeorm/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/typeorm/node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], + "node_modules/update-notifier/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "dependencies": { + "color-convert": "^1.9.0" }, "engines": { - "node": ">=14.17" + "node": ">=4" } }, - "node_modules/typescript-eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", - "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", + "node_modules/update-notifier/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.57.1", - "@typescript-eslint/parser": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "node": ">=4" } }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "node_modules/update-notifier/node_modules/ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, + "license": "MIT" + }, + "node_modules/update-notifier/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/update-notifier/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-notifier/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "node_modules/update-notifier/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=4" + } }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" + "node_modules/update-notifier/node_modules/is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^1.5.0" + }, + "bin": { + "is-ci": "bin.js" + } }, - "node_modules/union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "node_modules/update-notifier/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "license": "MIT", "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "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/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "dev": true, + "license": "MIT" + }, + "node_modules/url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prepend-http": "^1.0.1" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/union-value/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "node_modules/use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha512-ODgiYu03y5g76A1I9Gt0/chLCzQjvzDy7DsZGsLOE/1MrF6wriEskSncj1+/C58Xk/kPZDppSctDybCwOSaGAg==", - "dev": true, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", "dependencies": { - "crypto-random-string": "^1.0.0" + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=10.12.0" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" }, "engines": { - "node": ">=0.10.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/unset-value/node_modules/has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "isarray": "1.0.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/unset-value/node_modules/has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha512-N0XH6lqDtFH84JxptQoZYmloF4nzrQqqrAymNj+/gW60AO2AZgOcf4O/nUXJcYfyQkqvMo9lSupBZmmgvuVXlw==", + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=4", - "yarn": "*" + "node": ">=18" } }, - "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==", + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], "dev": true, - "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", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" ], + "dev": true, "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", - "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/update-notifier/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/update-notifier/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/update-notifier/node_modules/ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/update-notifier/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "dependencies": { - "color-name": "1.1.3" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/update-notifier/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/update-notifier/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.8.0" + "node": ">=18" } }, - "node_modules/update-notifier/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/update-notifier/node_modules/is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "dependencies": { - "ci-info": "^1.5.0" - }, - "bin": { - "is-ci": "bin.js" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/update-notifier/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", - "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==", + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "prepend-http": "^1.0.1" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.4.0" + "node": ">=18" } }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" ], + "dev": true, "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=10.12.0" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vite/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/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/walker": { @@ -13174,7 +15274,7 @@ "version": "2.8.3", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -13268,6 +15368,7 @@ "@slack/web-api": "^7.15.0", "axios": "^0.18.1", "body-parser": "^1.20.2", + "cors": "^2.8.6", "decimal.js": "^10.6.0", "easy-table": "^1.1.1", "express": "^4.18.2", @@ -13283,6 +15384,7 @@ }, "devDependencies": { "@types/body-parser": "^1.19.5", + "@types/cors": "^2.8.19", "@types/easy-table": "0.0.32", "@types/express": "^4.17.21", "@types/jest": "^24.0.15", @@ -13305,7 +15407,294 @@ "packages/frontend": { "name": "@mocker/frontend", "version": "1.0.0", - "devDependencies": {} + "dependencies": { + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.511.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "tailwind-merge": "^3.3.0", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@eslint/js": "^9.25.0", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "autoprefixer": "^10.4.21", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "typescript": "~5.8.3", + "typescript-eslint": "^8.30.1", + "vite": "^6.3.5" + } + }, + "packages/frontend/node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "packages/frontend/node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "packages/frontend/node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "packages/frontend/node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "packages/frontend/node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "packages/frontend/node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "packages/frontend/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "packages/frontend/node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "packages/frontend/node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "packages/frontend/node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "packages/frontend/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "packages/frontend/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "packages/frontend/node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/frontend/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" + } + }, + "packages/frontend/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "packages/frontend/node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } } } } diff --git a/package.json b/package.json index 73664088..2608c3e0 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "scripts": { "build": "npm run build --workspaces --if-present", "build:backend": "npm run build -w @mocker/backend", + "build:frontend": "npm run build -w @mocker/frontend", "build:prod": "npm run build:prod --workspaces --if-present", "build:prod:backend": "npm run build:prod -w @mocker/backend", "prepare": "husky", @@ -17,6 +18,7 @@ "test": "npm run test --workspaces --if-present", "test:backend": "npm run test -w @mocker/backend", "test:coverage": "npm run test:coverage -w @mocker/backend", + "dev:frontend": "npm run dev -w @mocker/frontend", "lint": "eslint --cache --cache-location node_modules/.cache/eslint/root/.eslintcache packages/**/*.ts", "lint:fix": "eslint --cache --cache-location node_modules/.cache/eslint/root/.eslintcache packages/**/*.ts --quiet --fix", "format:check": "prettier --cache --check \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", diff --git a/packages/backend/package.json b/packages/backend/package.json index 3326797c..071b9d98 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -25,6 +25,7 @@ "@slack/web-api": "^7.15.0", "axios": "^0.18.1", "body-parser": "^1.20.2", + "cors": "^2.8.6", "decimal.js": "^10.6.0", "easy-table": "^1.1.1", "express": "^4.18.2", @@ -40,6 +41,7 @@ }, "devDependencies": { "@types/body-parser": "^1.19.5", + "@types/cors": "^2.8.19", "@types/easy-table": "0.0.32", "@types/express": "^4.17.21", "@types/jest": "^24.0.15", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 2445822c..2bd9a40e 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,6 +1,7 @@ import 'reflect-metadata'; // Necessary for TypeORM entities. import 'dotenv/config'; import bodyParser from 'body-parser'; +import cors from 'cors'; import type { Application } from 'express'; import express from 'express'; @@ -29,10 +30,12 @@ import { logger } from './shared/logger/logger'; import { AIService } from './ai/ai.service'; import { portfolioController } from './portfolio/portfolio.controller'; import { hookController } from './hook/hook.controller'; +import { searchController } from './search/search.controller'; const app: Application = express(); const PORT = process.env.PORT || 3000; +app.use(cors({ origin: process.env.SEARCH_UI_ORIGIN || 'http://localhost:5173' })); app.use( bodyParser.urlencoded({ extended: true, @@ -48,6 +51,7 @@ app.use( }, }), ); +app.use('/search', searchController); app.use(signatureVerificationMiddleware); app.use('/ai', aiController); app.use('/clap', clapController); diff --git a/packages/backend/src/search/search.controller.ts b/packages/backend/src/search/search.controller.ts new file mode 100644 index 00000000..42242a63 --- /dev/null +++ b/packages/backend/src/search/search.controller.ts @@ -0,0 +1,31 @@ +import type { Router } from 'express'; +import express from 'express'; +import { SearchPersistenceService } from './search.persistence.service'; +import { logError } from '../shared/logger/error-logging'; +import { logger } from '../shared/logger/logger'; + +export const searchController: Router = express.Router(); + +const searchPersistenceService = new SearchPersistenceService(); +const searchLogger = logger.child({ module: 'SearchController' }); + +searchController.get('/messages', (req, res) => { + const { userName, channel, content, limit } = req.query; + + searchPersistenceService + .searchMessages({ + userName: typeof userName === 'string' ? userName : undefined, + channel: typeof channel === 'string' ? channel : undefined, + content: typeof content === 'string' ? content : undefined, + limit: typeof limit === 'string' ? parseInt(limit, 10) : undefined, + }) + .then((messages) => res.status(200).json(messages)) + .catch((e: unknown) => { + logError(searchLogger, 'Failed to search messages', e, { + userName, + channel, + content, + }); + res.status(500).send(); + }); +}); diff --git a/packages/backend/src/search/search.persistence.service.ts b/packages/backend/src/search/search.persistence.service.ts new file mode 100644 index 00000000..1fab4c15 --- /dev/null +++ b/packages/backend/src/search/search.persistence.service.ts @@ -0,0 +1,63 @@ +import { getRepository } from 'typeorm'; +import { Message } from '../shared/db/models/Message'; +import type { MessageWithName } from '../shared/models/message/message-with-name'; +import { logError } from '../shared/logger/error-logging'; +import { logger } from '../shared/logger/logger'; + +export interface MessageSearchParams { + userName?: string; + channel?: string; + content?: string; + limit?: number; +} + +export class SearchPersistenceService { + private logger = logger.child({ module: 'SearchPersistenceService' }); + + async searchMessages(params: MessageSearchParams): Promise { + const { userName, channel, content, limit = 100 } = params; + + const conditions: string[] = ["message.message != ''"]; + const queryParams: (string | number)[] = []; + + if (userName) { + conditions.push('slack_user.name LIKE ?'); + queryParams.push(`%${userName}%`); + } + + if (channel) { + conditions.push('message.channel LIKE ?'); + queryParams.push(`%${channel}%`); + } + + if (content) { + conditions.push('message.message LIKE ?'); + queryParams.push(`%${content}%`); + } + + const whereClause = conditions.join(' AND '); + + const query = ` + SELECT message.*, slack_user.name, slack_user.slackId + FROM message + INNER JOIN slack_user ON slack_user.id = message.userIdId + WHERE ${whereClause} + ORDER BY message.createdAt DESC + LIMIT ? + `; + + queryParams.push(limit); + + return getRepository(Message) + .query(query, queryParams) + .catch((e: unknown) => { + logError(this.logger, 'Failed to search messages', e, { + userName: params.userName, + channel: params.channel, + content: params.content, + limit: params.limit, + }); + throw e; + }); + } +} diff --git a/packages/frontend/.env.example b/packages/frontend/.env.example new file mode 100644 index 00000000..99cbcd54 --- /dev/null +++ b/packages/frontend/.env.example @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:3000 diff --git a/packages/frontend/index.html b/packages/frontend/index.html new file mode 100644 index 00000000..49783e65 --- /dev/null +++ b/packages/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Mocker - Message Search + + +
+ + + diff --git a/packages/frontend/package.json b/packages/frontend/package.json index bb7a16a8..0f7bc8c9 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -3,7 +3,39 @@ "version": "1.0.0", "description": "Mock your friends - Frontend", "private": true, - "scripts": {}, - "dependencies": {}, - "devDependencies": {} + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.511.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "tailwind-merge": "^3.3.0", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@eslint/js": "^9.25.0", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "autoprefixer": "^10.4.21", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "typescript": "~5.8.3", + "typescript-eslint": "^8.30.1", + "vite": "^6.3.5" + } } diff --git a/packages/frontend/postcss.config.js b/packages/frontend/postcss.config.js new file mode 100644 index 00000000..2aa7205d --- /dev/null +++ b/packages/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx new file mode 100644 index 00000000..55d9e759 --- /dev/null +++ b/packages/frontend/src/App.tsx @@ -0,0 +1,192 @@ +import { useState, useCallback } from 'react'; +import { Search, Loader2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Separator } from '@/components/ui/separator'; + +interface Message { + id: number; + message: string; + channel: string; + teamId: string; + createdAt: string; + name: string; + slackId: string; +} + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000'; + +function formatDate(dateString: string): string { + return new Date(dateString).toLocaleString(); +} + +export default function App() { + const [userName, setUserName] = useState(''); + const [channel, setChannel] = useState(''); + const [content, setContent] = useState(''); + const [messages, setMessages] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [hasSearched, setHasSearched] = useState(false); + + const handleSearch = useCallback(async () => { + setIsLoading(true); + setError(null); + + const params = new URLSearchParams(); + if (userName.trim()) params.set('userName', userName.trim()); + if (channel.trim()) params.set('channel', channel.trim()); + if (content.trim()) params.set('content', content.trim()); + + try { + const response = await fetch(`${API_BASE_URL}/search/messages?${params.toString()}`); + if (!response.ok) { + throw new Error(`Search failed: ${response.statusText}`); + } + const data: Message[] = (await response.json()) as Message[]; + setMessages(data); + setHasSearched(true); + } catch (err) { + setError(err instanceof Error ? err.message : 'An unexpected error occurred'); + } finally { + setIsLoading(false); + } + }, [userName, channel, content]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + void handleSearch(); + } + }; + + const activeFilters = [ + userName.trim() && { label: 'User', value: userName.trim() }, + channel.trim() && { label: 'Channel', value: channel.trim() }, + content.trim() && { label: 'Content', value: content.trim() }, + ].filter(Boolean) as { label: string; value: string }[]; + + return ( +
+
+
+

Message Search

+

Search through Slack messages by user, channel, or content.

+
+ + + + Search Filters + + Enter one or more filters to narrow your search. All filters are combined. + + + +
+
+ + setUserName(e.target.value)} + onKeyDown={handleKeyDown} + /> +
+
+ + setChannel(e.target.value)} + onKeyDown={handleKeyDown} + /> +
+
+ + setContent(e.target.value)} + onKeyDown={handleKeyDown} + /> +
+
+
+ + {activeFilters.length > 0 && ( +
+ {activeFilters.map((filter) => ( + + {filter.label}: {filter.value} + + ))} +
+ )} +
+
+
+ + {error && ( + + +

{error}

+
+
+ )} + + {hasSearched && ( + + + Results + + {messages.length === 0 + ? 'No messages found matching your search criteria.' + : `Found ${messages.length} message${messages.length === 1 ? '' : 's'}`} + + + {messages.length > 0 && ( + <> + + + + + + User + Channel + Message + Date + + + + {messages.map((msg) => ( + + {msg.name} + + #{msg.channel} + + + {msg.message} + + {formatDate(msg.createdAt)} + + ))} + +
+
+ + )} +
+ )} +
+
+ ); +} diff --git a/packages/frontend/src/components/ui/badge.tsx b/packages/frontend/src/components/ui/badge.tsx new file mode 100644 index 00000000..1917c24c --- /dev/null +++ b/packages/frontend/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const badgeVariants = cva( + 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', + { + variants: { + variant: { + default: 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', + secondary: 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/80', + destructive: + 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<'span'> & VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'span'; + + return ; +} + +export { Badge, badgeVariants }; diff --git a/packages/frontend/src/components/ui/button.tsx b/packages/frontend/src/components/ui/button.tsx new file mode 100644 index 00000000..29bc82f1 --- /dev/null +++ b/packages/frontend/src/components/ui/button.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', + destructive: + 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<'button'> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot : 'button'; + + return ; +} + +export { Button, buttonVariants }; diff --git a/packages/frontend/src/components/ui/card.tsx b/packages/frontend/src/components/ui/card.tsx new file mode 100644 index 00000000..321e0114 --- /dev/null +++ b/packages/frontend/src/components/ui/card.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Card({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { + return
; +} + +function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { + return
; +} + +function CardAction({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardContent({ className, ...props }: React.ComponentProps<'div'>) { + return
; +} + +function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; diff --git a/packages/frontend/src/components/ui/input.tsx b/packages/frontend/src/components/ui/input.tsx new file mode 100644 index 00000000..e8f37bad --- /dev/null +++ b/packages/frontend/src/components/ui/input.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Input({ className, type, ...props }: React.ComponentProps<'input'>) { + return ( + + ); +} + +export { Input }; diff --git a/packages/frontend/src/components/ui/label.tsx b/packages/frontend/src/components/ui/label.tsx new file mode 100644 index 00000000..8e3a70d0 --- /dev/null +++ b/packages/frontend/src/components/ui/label.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; + +import { cn } from '@/lib/utils'; + +function Label({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +export { Label }; diff --git a/packages/frontend/src/components/ui/separator.tsx b/packages/frontend/src/components/ui/separator.tsx new file mode 100644 index 00000000..cf51fabe --- /dev/null +++ b/packages/frontend/src/components/ui/separator.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import * as SeparatorPrimitive from '@radix-ui/react-separator'; + +import { cn } from '@/lib/utils'; + +function Separator({ + className, + orientation = 'horizontal', + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Separator }; diff --git a/packages/frontend/src/components/ui/table.tsx b/packages/frontend/src/components/ui/table.tsx new file mode 100644 index 00000000..80fd4124 --- /dev/null +++ b/packages/frontend/src/components/ui/table.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Table({ className, ...props }: React.ComponentProps<'table'>) { + return ( +
+ + + ); +} + +function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) { + return ; +} + +function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) { + return ; +} + +function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) { + return ( + tr]:last:border-b-0', className)} + {...props} + /> + ); +} + +function TableRow({ className, ...props }: React.ComponentProps<'tr'>) { + return ( + + ); +} + +function TableHead({ className, ...props }: React.ComponentProps<'th'>) { + return ( +
[role=checkbox]]:translate-y-[2px]', + className, + )} + {...props} + /> + ); +} + +function TableCell({ className, ...props }: React.ComponentProps<'td'>) { + return ( + [role=checkbox]]:translate-y-[2px]', className)} + {...props} + /> + ); +} + +function TableCaption({ className, ...props }: React.ComponentProps<'caption'>) { + return ( +
+ ); +} + +export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }; diff --git a/packages/frontend/src/index.css b/packages/frontend/src/index.css new file mode 100644 index 00000000..d924e243 --- /dev/null +++ b/packages/frontend/src/index.css @@ -0,0 +1,69 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --radius: 0.5rem; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/packages/frontend/src/lib/utils.ts b/packages/frontend/src/lib/utils.ts new file mode 100644 index 00000000..9ad0df42 --- /dev/null +++ b/packages/frontend/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/packages/frontend/src/main.tsx b/packages/frontend/src/main.tsx new file mode 100644 index 00000000..2239905c --- /dev/null +++ b/packages/frontend/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/packages/frontend/src/vite-env.d.ts b/packages/frontend/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/packages/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/frontend/tailwind.config.js b/packages/frontend/tailwind.config.js new file mode 100644 index 00000000..13df8673 --- /dev/null +++ b/packages/frontend/tailwind.config.js @@ -0,0 +1,57 @@ +/** @type {import('tailwindcss').Config} */ +export default { + darkMode: ['class'], + content: ['./index.html', './src/**/*.{ts,tsx,js,jsx}'], + theme: { + extend: { + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + 1: 'hsl(var(--chart-1))', + 2: 'hsl(var(--chart-2))', + 3: 'hsl(var(--chart-3))', + 4: 'hsl(var(--chart-4))', + 5: 'hsl(var(--chart-5))', + }, + }, + }, + }, + plugins: [require('tailwindcss-animate')], +}; diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json new file mode 100644 index 00000000..be42d05a --- /dev/null +++ b/packages/frontend/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/packages/frontend/tsconfig.node.json b/packages/frontend/tsconfig.node.json new file mode 100644 index 00000000..f315807f --- /dev/null +++ b/packages/frontend/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts new file mode 100644 index 00000000..95d423e2 --- /dev/null +++ b/packages/frontend/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +}); From 8eb60d2cb6e5b6e259f18a3852312f55f0cdfabd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:05:32 +0000 Subject: [PATCH 03/38] Add tests for search module to restore 80% branch coverage threshold Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/67674b97-52b7-4275-9ebc-5a3c2b53f865 --- .../src/search/search.controller.spec.ts | 59 +++++++++++++ .../search/search.persistence.service.spec.ts | 82 +++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 packages/backend/src/search/search.controller.spec.ts create mode 100644 packages/backend/src/search/search.persistence.service.spec.ts diff --git a/packages/backend/src/search/search.controller.spec.ts b/packages/backend/src/search/search.controller.spec.ts new file mode 100644 index 00000000..2f54beec --- /dev/null +++ b/packages/backend/src/search/search.controller.spec.ts @@ -0,0 +1,59 @@ +import express from 'express'; +import request from 'supertest'; + +const searchMessagesMock = jest.fn(); + +jest.mock('./search.persistence.service', () => ({ + SearchPersistenceService: jest.fn().mockImplementation(() => ({ + searchMessages: searchMessagesMock, + })), +})); + +import { searchController } from './search.controller'; + +describe('searchController', () => { + const app = express(); + app.use(express.json()); + app.use('/', searchController); + + beforeEach(() => jest.clearAllMocks()); + + it('returns 200 with messages when search succeeds', async () => { + const messages = [{ id: 1, message: 'hello', name: 'alice', channel: 'general' }]; + searchMessagesMock.mockResolvedValue(messages); + + const res = await request(app) + .get('/messages') + .query({ userName: 'alice', channel: 'general', content: 'hello', limit: '10' }); + + expect(res.status).toBe(200); + expect(res.body).toEqual(messages); + expect(searchMessagesMock).toHaveBeenCalledWith({ + userName: 'alice', + channel: 'general', + content: 'hello', + limit: 10, + }); + }); + + it('passes undefined for query params that are not strings', async () => { + searchMessagesMock.mockResolvedValue([]); + + await request(app).get('/messages').expect(200); + + expect(searchMessagesMock).toHaveBeenCalledWith({ + userName: undefined, + channel: undefined, + content: undefined, + limit: undefined, + }); + }); + + it('returns 500 when search throws an error', async () => { + searchMessagesMock.mockRejectedValue(new Error('DB failure')); + + const res = await request(app).get('/messages').query({ userName: 'bob' }); + + expect(res.status).toBe(500); + }); +}); diff --git a/packages/backend/src/search/search.persistence.service.spec.ts b/packages/backend/src/search/search.persistence.service.spec.ts new file mode 100644 index 00000000..3b22edec --- /dev/null +++ b/packages/backend/src/search/search.persistence.service.spec.ts @@ -0,0 +1,82 @@ +import { getRepository } from 'typeorm'; +import { SearchPersistenceService } from './search.persistence.service'; + +jest.mock('typeorm', () => { + const actual = jest.requireActual('typeorm'); + return { + ...actual, + getRepository: jest.fn(), + }; +}); + +describe('SearchPersistenceService', () => { + let service: SearchPersistenceService; + const query = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + service = new SearchPersistenceService(); + (getRepository as jest.Mock).mockReturnValue({ query }); + }); + + it('searches with no filters (default conditions only)', async () => { + query.mockResolvedValue([{ id: 1, message: 'hello', name: 'alice' }]); + + const result = await service.searchMessages({}); + + expect(query).toHaveBeenCalledWith(expect.stringContaining("message.message != ''"), [100]); + expect(result).toEqual([{ id: 1, message: 'hello', name: 'alice' }]); + }); + + it('applies userName LIKE filter when userName is provided', async () => { + query.mockResolvedValue([]); + + await service.searchMessages({ userName: 'alice' }); + + expect(query).toHaveBeenCalledWith(expect.stringContaining('slack_user.name LIKE ?'), ['%alice%', 100]); + }); + + it('applies channel LIKE filter when channel is provided', async () => { + query.mockResolvedValue([]); + + await service.searchMessages({ channel: 'general' }); + + expect(query).toHaveBeenCalledWith(expect.stringContaining('message.channel LIKE ?'), ['%general%', 100]); + }); + + it('applies content LIKE filter when content is provided', async () => { + query.mockResolvedValue([]); + + await service.searchMessages({ content: 'hello' }); + + expect(query).toHaveBeenCalledWith(expect.stringContaining('message.message LIKE ?'), ['%hello%', 100]); + }); + + it('applies all three filters combined', async () => { + query.mockResolvedValue([]); + + await service.searchMessages({ userName: 'alice', channel: 'general', content: 'hello' }); + + const [sql, params] = (query as jest.Mock).mock.calls[0] as [string, unknown[]]; + expect(sql).toContain('slack_user.name LIKE ?'); + expect(sql).toContain('message.channel LIKE ?'); + expect(sql).toContain('message.message LIKE ?'); + expect(params).toEqual(['%alice%', '%general%', '%hello%', 100]); + }); + + it('uses the provided limit instead of the default', async () => { + query.mockResolvedValue([]); + + await service.searchMessages({ limit: 25 }); + + const [, params] = (query as jest.Mock).mock.calls[0] as [string, unknown[]]; + expect(params[params.length - 1]).toBe(25); + }); + + it('rethrows and logs errors from the database', async () => { + const error = new Error('DB error'); + query.mockRejectedValue(error); + + await expect(service.searchMessages({})).rejects.toThrow('DB error'); + }); +}); From d8c74adc4c8323e274afb869e6789ad2637b4ef4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:27:06 +0000 Subject: [PATCH 04/38] Add Slack OAuth auth gated to dabros2016.slack.com workspace Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/76b05824-fba9-448b-85e5-c65893a53e76 --- package-lock.json | 28 ++++ packages/backend/package.json | 1 + .../backend/src/auth/auth.controller.spec.ts | 139 ++++++++++++++++++ packages/backend/src/auth/auth.controller.ts | 107 ++++++++++++++ packages/backend/src/index.ts | 20 ++- .../shared/middleware/authMiddleware.spec.ts | 74 ++++++++++ .../src/shared/middleware/authMiddleware.ts | 23 +++ .../src/shared/utils/session-token.spec.ts | 83 +++++++++++ .../backend/src/shared/utils/session-token.ts | 64 ++++++++ packages/frontend/src/App.tsx | 65 +++++++- .../frontend/src/components/LoginPage.tsx | 39 +++++ 11 files changed, 636 insertions(+), 7 deletions(-) create mode 100644 packages/backend/src/auth/auth.controller.spec.ts create mode 100644 packages/backend/src/auth/auth.controller.ts create mode 100644 packages/backend/src/shared/middleware/authMiddleware.spec.ts create mode 100644 packages/backend/src/shared/middleware/authMiddleware.ts create mode 100644 packages/backend/src/shared/utils/session-token.spec.ts create mode 100644 packages/backend/src/shared/utils/session-token.ts create mode 100644 packages/frontend/src/components/LoginPage.tsx diff --git a/package-lock.json b/package-lock.json index 7350035e..7ba26eeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6201,6 +6201,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -7470,6 +7488,15 @@ "url": "https://opencollective.com/ioredis" } }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -15372,6 +15399,7 @@ "decimal.js": "^10.6.0", "easy-table": "^1.1.1", "express": "^4.18.2", + "express-rate-limit": "^8.3.1", "ioredis": "^5.0.4", "moment": "^2.24.0", "mysql": "^2.17.1", diff --git a/packages/backend/package.json b/packages/backend/package.json index 071b9d98..3648ab0e 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -29,6 +29,7 @@ "decimal.js": "^10.6.0", "easy-table": "^1.1.1", "express": "^4.18.2", + "express-rate-limit": "^8.3.1", "ioredis": "^5.0.4", "moment": "^2.24.0", "mysql": "^2.17.1", diff --git a/packages/backend/src/auth/auth.controller.spec.ts b/packages/backend/src/auth/auth.controller.spec.ts new file mode 100644 index 00000000..e7ce4d24 --- /dev/null +++ b/packages/backend/src/auth/auth.controller.spec.ts @@ -0,0 +1,139 @@ +import express from 'express'; +import request from 'supertest'; +import Axios from 'axios'; + +jest.mock('axios'); +jest.mock('../shared/utils/session-token', () => ({ + createSessionToken: jest.fn().mockReturnValue('mock-session-token'), +})); + +import { authController } from './auth.controller'; + +describe('authController', () => { + const app = express(); + app.use(express.json()); + app.use('/', authController); + + const OLD_ENV = process.env; + + beforeEach(() => { + jest.clearAllMocks(); + process.env = { + ...OLD_ENV, + SLACK_CLIENT_ID: 'test-client-id', + SLACK_CLIENT_SECRET: 'test-client-secret', + SLACK_REDIRECT_URI: 'http://localhost:3000/auth/slack/callback', + SEARCH_FRONTEND_URL: 'http://localhost:5173', + }; + }); + + afterAll(() => { + process.env = OLD_ENV; + }); + + describe('GET /slack', () => { + it('redirects to Slack OAuth URL with client_id and user_scope', async () => { + const res = await request(app).get('/slack'); + expect(res.status).toBe(302); + expect(res.headers.location).toContain('slack.com/oauth/v2/authorize'); + expect(res.headers.location).toContain('client_id=test-client-id'); + expect(res.headers.location).toContain('user_scope=identity.basic'); + }); + + it('returns 500 when SLACK_CLIENT_ID is not set', async () => { + delete process.env.SLACK_CLIENT_ID; + const res = await request(app).get('/slack'); + expect(res.status).toBe(500); + }); + }); + + describe('GET /slack/callback', () => { + it('redirects to frontend with token on successful OAuth', async () => { + (Axios.post as jest.Mock).mockResolvedValue({ + data: { ok: true, authed_user: { id: 'U123', access_token: 'xoxp-token' } }, + }); + (Axios.get as jest.Mock).mockResolvedValue({ + data: { ok: true, user: { id: 'U123', name: 'alice' }, team: { domain: 'dabros2016', id: 'T123' } }, + }); + + const res = await request(app).get('/slack/callback').query({ code: 'valid-code' }); + + expect(res.status).toBe(302); + expect(res.headers.location).toContain('token=mock-session-token'); + }); + + it('redirects with auth_error=access_denied when error param is present', async () => { + const res = await request(app).get('/slack/callback').query({ error: 'access_denied' }); + expect(res.status).toBe(302); + expect(res.headers.location).toContain('auth_error=access_denied'); + }); + + it('redirects with auth_error=access_denied when code is missing', async () => { + const res = await request(app).get('/slack/callback'); + expect(res.status).toBe(302); + expect(res.headers.location).toContain('auth_error=access_denied'); + }); + + it('returns 500 when SLACK_CLIENT_SECRET is not set', async () => { + delete process.env.SLACK_CLIENT_SECRET; + const res = await request(app).get('/slack/callback').query({ code: 'some-code' }); + expect(res.status).toBe(500); + }); + + it('returns 500 when SLACK_CLIENT_ID is not set', async () => { + delete process.env.SLACK_CLIENT_ID; + const res = await request(app).get('/slack/callback').query({ code: 'some-code' }); + expect(res.status).toBe(500); + }); + + it('redirects with auth_error=token_exchange_failed when Slack token response is not ok', async () => { + (Axios.post as jest.Mock).mockResolvedValue({ data: { ok: false } }); + + const res = await request(app).get('/slack/callback').query({ code: 'bad-code' }); + expect(res.status).toBe(302); + expect(res.headers.location).toContain('auth_error=token_exchange_failed'); + }); + + it('redirects with auth_error=token_exchange_failed when authed_user is missing', async () => { + (Axios.post as jest.Mock).mockResolvedValue({ data: { ok: true } }); + + const res = await request(app).get('/slack/callback').query({ code: 'bad-code' }); + expect(res.status).toBe(302); + expect(res.headers.location).toContain('auth_error=token_exchange_failed'); + }); + + it('redirects with auth_error=unauthorized_workspace when team domain is wrong', async () => { + (Axios.post as jest.Mock).mockResolvedValue({ + data: { ok: true, authed_user: { id: 'U999', access_token: 'xoxp-other' } }, + }); + (Axios.get as jest.Mock).mockResolvedValue({ + data: { ok: true, user: { id: 'U999', name: 'bob' }, team: { domain: 'otherworkspace', id: 'T999' } }, + }); + + const res = await request(app).get('/slack/callback').query({ code: 'wrong-team-code' }); + expect(res.status).toBe(302); + expect(res.headers.location).toContain('auth_error=unauthorized_workspace'); + }); + + it('redirects with auth_error=unauthorized_workspace when identity response is not ok', async () => { + (Axios.post as jest.Mock).mockResolvedValue({ + data: { ok: true, authed_user: { id: 'U123', access_token: 'xoxp-token' } }, + }); + (Axios.get as jest.Mock).mockResolvedValue({ + data: { ok: false }, + }); + + const res = await request(app).get('/slack/callback').query({ code: 'bad-identity' }); + expect(res.status).toBe(302); + expect(res.headers.location).toContain('auth_error=unauthorized_workspace'); + }); + + it('redirects with auth_error=server_error when an unexpected exception is thrown', async () => { + (Axios.post as jest.Mock).mockRejectedValue(new Error('Network failure')); + + const res = await request(app).get('/slack/callback').query({ code: 'throw-code' }); + expect(res.status).toBe(302); + expect(res.headers.location).toContain('auth_error=server_error'); + }); + }); +}); diff --git a/packages/backend/src/auth/auth.controller.ts b/packages/backend/src/auth/auth.controller.ts new file mode 100644 index 00000000..09c01492 --- /dev/null +++ b/packages/backend/src/auth/auth.controller.ts @@ -0,0 +1,107 @@ +import type { Router } from 'express'; +import express from 'express'; +import Axios from 'axios'; +import { createSessionToken } from '../shared/utils/session-token'; +import { logError } from '../shared/logger/error-logging'; +import { logger } from '../shared/logger/logger'; + +export const authController: Router = express.Router(); +const authLogger = logger.child({ module: 'AuthController' }); + +const ALLOWED_TEAM_DOMAIN = 'dabros2016'; +const SLACK_AUTH_URL = 'https://slack.com/oauth/v2/authorize'; +const SLACK_TOKEN_URL = 'https://slack.com/api/oauth.v2.access'; +const SLACK_IDENTITY_URL = 'https://slack.com/api/users.identity'; + +interface SlackTokenResponse { + ok: boolean; + authed_user?: { + id: string; + access_token: string; + }; +} + +interface SlackIdentityResponse { + ok: boolean; + user?: { + id: string; + name: string; + }; + team?: { + domain: string; + id: string; + }; +} + +authController.get('/slack', (_req, res) => { + const clientId = process.env.SLACK_CLIENT_ID; + const redirectUri = process.env.SLACK_REDIRECT_URI ?? 'http://localhost:3000/auth/slack/callback'; + + if (!clientId) { + res.status(500).send('Slack OAuth is not configured'); + return; + } + + const params = new URLSearchParams({ + client_id: clientId, + user_scope: 'identity.basic', + redirect_uri: redirectUri, + }); + + res.redirect(`${SLACK_AUTH_URL}?${params.toString()}`); +}); + +authController.get('/slack/callback', (req, res) => { + const frontendUrl = process.env.SEARCH_FRONTEND_URL ?? 'http://localhost:5173'; + + void (async () => { + const { code, error } = req.query; + + if (error || typeof code !== 'string') { + res.redirect(`${frontendUrl}?auth_error=access_denied`); + return; + } + + const clientId = process.env.SLACK_CLIENT_ID; + const clientSecret = process.env.SLACK_CLIENT_SECRET; + const redirectUri = process.env.SLACK_REDIRECT_URI ?? 'http://localhost:3000/auth/slack/callback'; + + if (!clientId || !clientSecret) { + res.status(500).send('Slack OAuth is not configured'); + return; + } + + const tokenResponse = await Axios.post( + SLACK_TOKEN_URL, + new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, + code, + redirect_uri: redirectUri, + }).toString(), + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }, + ); + + if (!tokenResponse.data.ok || !tokenResponse.data.authed_user) { + res.redirect(`${frontendUrl}?auth_error=token_exchange_failed`); + return; + } + + const identityResponse = await Axios.get(SLACK_IDENTITY_URL, { + headers: { Authorization: `Bearer ${tokenResponse.data.authed_user.access_token}` }, + }); + + const teamDomain = identityResponse.data.team?.domain; + const userId = identityResponse.data.user?.id; + if (!identityResponse.data.ok || teamDomain !== ALLOWED_TEAM_DOMAIN || !userId) { + res.redirect(`${frontendUrl}?auth_error=unauthorized_workspace`); + return; + } + + const sessionToken = createSessionToken(userId, teamDomain); + res.redirect(`${frontendUrl}?token=${sessionToken}`); + })().catch((e: unknown) => { + logError(authLogger, 'Slack OAuth callback failed', e, {}); + res.redirect(`${frontendUrl}?auth_error=server_error`); + }); +}); diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 2bd9a40e..d2de242c 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -5,6 +5,7 @@ import cors from 'cors'; import type { Application } from 'express'; import express from 'express'; +import { rateLimit } from 'express-rate-limit'; import { createConnection, getConnectionOptions } from 'typeorm'; import type { RequestWithRawBody } from './shared/models/express/RequestWithRawBody'; import { aiController } from './ai/ai.controller'; @@ -31,6 +32,8 @@ import { AIService } from './ai/ai.service'; import { portfolioController } from './portfolio/portfolio.controller'; import { hookController } from './hook/hook.controller'; import { searchController } from './search/search.controller'; +import { authController } from './auth/auth.controller'; +import { authMiddleware } from './shared/middleware/authMiddleware'; const app: Application = express(); const PORT = process.env.PORT || 3000; @@ -51,7 +54,22 @@ app.use( }, }), ); -app.use('/search', searchController); +const authRateLimit = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 20, + standardHeaders: true, + legacyHeaders: false, +}); + +const searchRateLimit = rateLimit({ + windowMs: 60 * 1000, // 1 minute + max: 60, + standardHeaders: true, + legacyHeaders: false, +}); + +app.use('/auth', authRateLimit, authController); +app.use('/search', searchRateLimit, authMiddleware, searchController); app.use(signatureVerificationMiddleware); app.use('/ai', aiController); app.use('/clap', clapController); diff --git a/packages/backend/src/shared/middleware/authMiddleware.spec.ts b/packages/backend/src/shared/middleware/authMiddleware.spec.ts new file mode 100644 index 00000000..1fcc4507 --- /dev/null +++ b/packages/backend/src/shared/middleware/authMiddleware.spec.ts @@ -0,0 +1,74 @@ +import type { Request, Response, NextFunction } from 'express'; +import { authMiddleware } from './authMiddleware'; +import { createSessionToken } from '../utils/session-token'; + +type AuthReq = Parameters[0]; +type AuthRes = Parameters[1]; + +const makeReq = (authorization?: string): AuthReq => ({ headers: { authorization } }) as AuthReq; + +const makeRes = (): AuthRes => { + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + } as unknown as AuthRes; + return res; +}; + +describe('authMiddleware', () => { + const OLD_ENV = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...OLD_ENV, SEARCH_AUTH_SECRET: 'test-secret' }; + }); + + afterAll(() => { + process.env = OLD_ENV; + }); + + it('calls next for a valid Bearer token', () => { + const token = createSessionToken('U1', 'dabros2016'); + const req = makeReq(`Bearer ${token}`); + const res = makeRes(); + const next = jest.fn() as unknown as NextFunction; + + authMiddleware(req as Request, res as Response, next); + + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + + it('returns 401 when Authorization header is missing', () => { + const req = makeReq(undefined); + const res = makeRes(); + const next = jest.fn() as unknown as NextFunction; + + authMiddleware(req as Request, res as Response, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(401); + }); + + it('returns 401 when Authorization header does not start with Bearer', () => { + const req = makeReq('Basic sometoken'); + const res = makeRes(); + const next = jest.fn() as unknown as NextFunction; + + authMiddleware(req as Request, res as Response, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(401); + }); + + it('returns 401 for an invalid token', () => { + const req = makeReq('Bearer invalid.token'); + const res = makeRes(); + const next = jest.fn() as unknown as NextFunction; + + authMiddleware(req as Request, res as Response, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(401); + }); +}); diff --git a/packages/backend/src/shared/middleware/authMiddleware.ts b/packages/backend/src/shared/middleware/authMiddleware.ts new file mode 100644 index 00000000..1de927d9 --- /dev/null +++ b/packages/backend/src/shared/middleware/authMiddleware.ts @@ -0,0 +1,23 @@ +import type { Request, Response, NextFunction } from 'express'; +import { verifySessionToken } from '../utils/session-token'; +import { logger } from '../logger/logger'; + +const authLogger = logger.child({ module: 'AuthMiddleware' }); + +export const authMiddleware = (req: Request, res: Response, next: NextFunction): void => { + const authHeader = req.headers.authorization; + if (!authHeader?.startsWith('Bearer ')) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + + const token = authHeader.slice(7); + const session = verifySessionToken(token); + if (!session) { + authLogger.warn('Invalid or expired session token'); + res.status(401).json({ error: 'Unauthorized' }); + return; + } + + next(); +}; diff --git a/packages/backend/src/shared/utils/session-token.spec.ts b/packages/backend/src/shared/utils/session-token.spec.ts new file mode 100644 index 00000000..b5b37b36 --- /dev/null +++ b/packages/backend/src/shared/utils/session-token.spec.ts @@ -0,0 +1,83 @@ +import crypto from 'crypto'; +import { createSessionToken, verifySessionToken } from './session-token'; + +describe('session-token', () => { + const OLD_ENV = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...OLD_ENV, SEARCH_AUTH_SECRET: 'test-secret' }; + }); + + afterAll(() => { + process.env = OLD_ENV; + }); + + describe('createSessionToken', () => { + it('returns a dot-separated base64url payload and signature', () => { + const token = createSessionToken('U123', 'testworkspace'); + expect(token).toMatch(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/); + }); + + it('embeds userId and teamDomain in the payload', () => { + const token = createSessionToken('U456', 'myteam'); + const [payload] = token.split('.'); + const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString()); + expect(decoded.userId).toBe('U456'); + expect(decoded.teamDomain).toBe('myteam'); + }); + + it('sets an expiry timestamp in the future', () => { + const before = Date.now(); + const token = createSessionToken('U1', 'team'); + const after = Date.now(); + const [payload] = token.split('.'); + const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString()); + expect(decoded.exp).toBeGreaterThan(before); + expect(decoded.exp).toBeGreaterThan(after); + }); + }); + + describe('verifySessionToken', () => { + it('returns payload for a valid token', () => { + const token = createSessionToken('U789', 'goodteam'); + const result = verifySessionToken(token); + expect(result).not.toBeNull(); + expect(result?.userId).toBe('U789'); + expect(result?.teamDomain).toBe('goodteam'); + }); + + it('returns null for a token with no dot separator', () => { + expect(verifySessionToken('nodot')).toBeNull(); + }); + + it('returns null when the token starts with a dot', () => { + expect(verifySessionToken('.nopayload')).toBeNull(); + }); + + it('returns null for a tampered signature', () => { + const token = createSessionToken('U1', 'team'); + const tampered = token.slice(0, -4) + 'XXXX'; + expect(verifySessionToken(tampered)).toBeNull(); + }); + + it('returns null for a malformed payload that is not valid base64url JSON', () => { + const fakeSig = 'c2lnbmF0dXJl'; + expect(verifySessionToken(`notbase64url!!!!.${fakeSig}`)).toBeNull(); + }); + + it('returns null for an expired token', () => { + jest.useFakeTimers(); + const token = createSessionToken('U1', 'team'); + jest.advanceTimersByTime(25 * 60 * 60 * 1000); // 25 hours + expect(verifySessionToken(token)).toBeNull(); + jest.useRealTimers(); + }); + + it('returns null for a payload that is valid JSON but missing required fields', () => { + const badPayload = Buffer.from(JSON.stringify({ foo: 'bar' })).toString('base64url'); + const sig = crypto.createHmac('sha256', 'test-secret').update(badPayload).digest('base64url'); + expect(verifySessionToken(`${badPayload}.${sig}`)).toBeNull(); + }); + }); +}); diff --git a/packages/backend/src/shared/utils/session-token.ts b/packages/backend/src/shared/utils/session-token.ts new file mode 100644 index 00000000..b59ff55a --- /dev/null +++ b/packages/backend/src/shared/utils/session-token.ts @@ -0,0 +1,64 @@ +import crypto from 'crypto'; +import { logger } from '../logger/logger'; + +const TOKEN_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours +const secretLogger = logger.child({ module: 'SessionToken' }); + +function getSecret(): string { + const secret = process.env.SEARCH_AUTH_SECRET; + if (!secret) { + secretLogger.warn('SEARCH_AUTH_SECRET is not set — session tokens are insecure. Set this variable in production.'); + return 'insecure-default-secret'; + } + return secret; +} + +export function createSessionToken(userId: string, teamDomain: string): string { + const payload = Buffer.from(JSON.stringify({ userId, teamDomain, exp: Date.now() + TOKEN_TTL_MS })).toString( + 'base64url', + ); + const sig = crypto.createHmac('sha256', getSecret()).update(payload).digest('base64url'); + return `${payload}.${sig}`; +} + +export interface SessionPayload { + userId: string; + teamDomain: string; + exp: number; +} + +function isSessionPayload(value: unknown): value is SessionPayload { + if (!value || typeof value !== 'object') { + return false; + } + return ( + typeof Reflect.get(value, 'userId') === 'string' && + typeof Reflect.get(value, 'teamDomain') === 'string' && + typeof Reflect.get(value, 'exp') === 'number' + ); +} + +export function verifySessionToken(token: string): SessionPayload | null { + const dotIndex = token.indexOf('.'); + if (dotIndex <= 0) return null; + + const payload = token.substring(0, dotIndex); + const sig = token.substring(dotIndex + 1); + if (!sig) return null; + + const expected = crypto.createHmac('sha256', getSecret()).update(payload).digest('base64url'); + try { + if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) return null; + } catch { + return null; + } + + try { + const parsed: unknown = JSON.parse(Buffer.from(payload, 'base64url').toString()); + if (!isSessionPayload(parsed)) return null; + if (parsed.exp < Date.now()) return null; + return parsed; + } catch { + return null; + } +} diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 55d9e759..3eb81d64 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -1,5 +1,5 @@ -import { useState, useCallback } from 'react'; -import { Search, Loader2 } from 'lucide-react'; +import { useState, useCallback, useEffect } from 'react'; +import { Search, Loader2, LogOut } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -7,6 +7,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Badge } from '@/components/ui/badge'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Separator } from '@/components/ui/separator'; +import { LoginPage } from '@/components/LoginPage'; interface Message { id: number; @@ -24,7 +25,15 @@ function formatDate(dateString: string): string { return new Date(dateString).toLocaleString(); } +const AUTH_TOKEN_KEY = 'auth_token'; + +function getStoredToken(): string | null { + return localStorage.getItem(AUTH_TOKEN_KEY); +} + export default function App() { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [authError, setAuthError] = useState(undefined); const [userName, setUserName] = useState(''); const [channel, setChannel] = useState(''); const [content, setContent] = useState(''); @@ -33,6 +42,32 @@ export default function App() { const [error, setError] = useState(null); const [hasSearched, setHasSearched] = useState(false); + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const tokenFromUrl = urlParams.get('token'); + const errorFromUrl = urlParams.get('auth_error') ?? undefined; + + if (tokenFromUrl) { + localStorage.setItem(AUTH_TOKEN_KEY, tokenFromUrl); + window.history.replaceState({}, '', window.location.pathname); + setIsAuthenticated(true); + } else if (getStoredToken()) { + setIsAuthenticated(true); + } + + if (errorFromUrl) { + setAuthError(errorFromUrl); + window.history.replaceState({}, '', window.location.pathname); + } + }, []); + + const handleLogout = useCallback(() => { + localStorage.removeItem(AUTH_TOKEN_KEY); + setIsAuthenticated(false); + setMessages([]); + setHasSearched(false); + }, []); + const handleSearch = useCallback(async () => { setIsLoading(true); setError(null); @@ -43,7 +78,15 @@ export default function App() { if (content.trim()) params.set('content', content.trim()); try { - const response = await fetch(`${API_BASE_URL}/search/messages?${params.toString()}`); + const token = getStoredToken() ?? ''; + const response = await fetch(`${API_BASE_URL}/search/messages?${params.toString()}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (response.status === 401) { + localStorage.removeItem(AUTH_TOKEN_KEY); + setIsAuthenticated(false); + return; + } if (!response.ok) { throw new Error(`Search failed: ${response.statusText}`); } @@ -69,12 +112,22 @@ export default function App() { content.trim() && { label: 'Content', value: content.trim() }, ].filter(Boolean) as { label: string; value: string }[]; + if (!isAuthenticated) { + return ; + } + return (
-
-

Message Search

-

Search through Slack messages by user, channel, or content.

+
+
+

Message Search

+

Search through Slack messages by user, channel, or content.

+
+
diff --git a/packages/frontend/src/components/LoginPage.tsx b/packages/frontend/src/components/LoginPage.tsx new file mode 100644 index 00000000..933fb0cb --- /dev/null +++ b/packages/frontend/src/components/LoginPage.tsx @@ -0,0 +1,39 @@ +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000'; + +const AUTH_ERROR_MESSAGES: Record = { + access_denied: 'Access was denied. Please try again.', + token_exchange_failed: 'Authentication failed. Please try again.', + unauthorized_workspace: 'Only members of the dabros2016.slack.com workspace may access this app.', + server_error: 'An unexpected error occurred. Please try again.', +}; + +interface LoginPageProps { + authError?: string; +} + +export function LoginPage({ authError }: LoginPageProps) { + const loginUrl = `${API_BASE_URL}/auth/slack`; + const errorMessage = authError + ? (AUTH_ERROR_MESSAGES[authError] ?? 'Authentication failed. Please try again.') + : null; + + return ( +
+ + + Message Search + Sign in with your dabros2016.slack.com account to search messages. + + + {errorMessage &&

{errorMessage}

} + + + +
+
+
+ ); +} From f06f6b36a15a5764f5fa6a9c6dd2c2426bb39bd8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:43:58 +0000 Subject: [PATCH 05/38] Address code review feedback: scope CORS, named consts, rename cn, fix title, add frontend scripts Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/0812ae7b-89b6-4c45-af85-c62e3a618fa6 --- eslint.config.js | 1 - package-lock.json | 367 +++++++++++++++++- package.json | 6 +- packages/backend/src/index.ts | 14 +- packages/frontend/eslint.config.js | 25 ++ packages/frontend/index.html | 2 +- packages/frontend/package.json | 9 +- packages/frontend/src/components/ui/badge.tsx | 4 +- .../frontend/src/components/ui/button.tsx | 6 +- packages/frontend/src/components/ui/card.tsx | 29 +- packages/frontend/src/components/ui/input.tsx | 4 +- packages/frontend/src/components/ui/label.tsx | 4 +- .../frontend/src/components/ui/separator.tsx | 4 +- packages/frontend/src/components/ui/table.tsx | 30 +- packages/frontend/src/lib/utils.ts | 2 +- 15 files changed, 469 insertions(+), 38 deletions(-) create mode 100644 packages/frontend/eslint.config.js diff --git a/eslint.config.js b/eslint.config.js index eb7685f6..6d237c25 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -11,7 +11,6 @@ module.exports = tseslint.config( '**/coverage/**', '**/*.d.ts', 'packages/backend/scripts/*.js', - 'packages/frontend/**', ], }, js.configs.recommended, diff --git a/package-lock.json b/package-lock.json index 7ba26eeb..ddd08c46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2697,6 +2697,13 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@textlint/ast-node-types": { "version": "13.4.1", "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-13.4.1.tgz", @@ -2787,6 +2794,17 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -2814,6 +2832,13 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/easy-table": { "version": "0.0.32", "resolved": "https://registry.npmjs.org/@types/easy-table/-/easy-table-0.0.32.tgz", @@ -3373,6 +3398,119 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.1.tgz", + "integrity": "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.1", + "@vitest/utils": "4.1.1", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.1.tgz", + "integrity": "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.1.tgz", + "integrity": "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.1.tgz", + "integrity": "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.1", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.1.tgz", + "integrity": "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.1", + "@vitest/utils": "4.1.1", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.1.tgz", + "integrity": "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.1.tgz", + "integrity": "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.1", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -3673,6 +3811,16 @@ "dev": true, "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -4504,6 +4652,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5695,6 +5853,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -6009,6 +6174,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -6155,6 +6330,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -9782,6 +9967,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -10538,6 +10733,17 @@ "node": ">=0.10.0" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -10901,6 +11107,13 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -12330,6 +12543,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -12665,6 +12885,13 @@ "node": ">=8" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -12721,6 +12948,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -13340,6 +13574,13 @@ "node": ">=0.10.0" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", @@ -13395,6 +13636,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -14869,6 +15120,101 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vitest": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.1.tgz", + "integrity": "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.1", + "@vitest/mocker": "4.1.1", + "@vitest/pretty-format": "4.1.1", + "@vitest/runner": "4.1.1", + "@vitest/snapshot": "4.1.1", + "@vitest/spy": "4.1.1", + "@vitest/utils": "4.1.1", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.1", + "@vitest/browser-preview": "4.1.1", + "@vitest/browser-webdriverio": "4.1.1", + "@vitest/ui": "4.1.1", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -14950,6 +15296,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/widest-line": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", @@ -15458,10 +15821,12 @@ "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", "postcss": "^8.5.3", + "prettier": "^3.5.3", "tailwindcss": "^3.4.17", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", - "vite": "^6.3.5" + "vite": "^6.3.5", + "vitest": "^4.1.1" } }, "packages/frontend/node_modules/@eslint/config-array": { diff --git a/package.json b/package.json index 2608c3e0..83757776 100644 --- a/package.json +++ b/package.json @@ -40,10 +40,14 @@ "typescript-eslint": "^8.57.1" }, "lint-staged": { - "**/*.{ts,tsx,js,jsx}": [ + "packages/backend/**/*.{ts,js}": [ "eslint --quiet --fix", "prettier --write" ], + "packages/frontend/**/*.{ts,tsx,js,jsx}": [ + "eslint --config packages/frontend/eslint.config.js --quiet --fix", + "prettier --write" + ], "**/*.{json,md,yml,yaml}": [ "prettier --write" ] diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index d2de242c..a5fecdb7 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -38,7 +38,8 @@ import { authMiddleware } from './shared/middleware/authMiddleware'; const app: Application = express(); const PORT = process.env.PORT || 3000; -app.use(cors({ origin: process.env.SEARCH_UI_ORIGIN || 'http://localhost:5173' })); +const searchCors = cors({ origin: process.env.SEARCH_UI_ORIGIN || 'http://localhost:5173' }); + app.use( bodyParser.urlencoded({ extended: true, @@ -54,22 +55,25 @@ app.use( }, }), ); +const AUTH_RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes +const SEARCH_RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute + const authRateLimit = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes + windowMs: AUTH_RATE_LIMIT_WINDOW_MS, max: 20, standardHeaders: true, legacyHeaders: false, }); const searchRateLimit = rateLimit({ - windowMs: 60 * 1000, // 1 minute + windowMs: SEARCH_RATE_LIMIT_WINDOW_MS, max: 60, standardHeaders: true, legacyHeaders: false, }); -app.use('/auth', authRateLimit, authController); -app.use('/search', searchRateLimit, authMiddleware, searchController); +app.use('/auth', searchCors, authRateLimit, authController); +app.use('/search', searchCors, searchRateLimit, authMiddleware, searchController); app.use(signatureVerificationMiddleware); app.use('/ai', aiController); app.use('/clap', clapController); diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js new file mode 100644 index 00000000..ea7d18ef --- /dev/null +++ b/packages/frontend/eslint.config.js @@ -0,0 +1,25 @@ +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'; + +export default tseslint.config( + { ignores: ['dist', 'coverage'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + }, + }, +); diff --git a/packages/frontend/index.html b/packages/frontend/index.html index 49783e65..598587da 100644 --- a/packages/frontend/index.html +++ b/packages/frontend/index.html @@ -4,7 +4,7 @@ - Mocker - Message Search + muzzle.lol - Message Search
diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 0f7bc8c9..4145b881 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -8,6 +8,11 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", + "lint:fix": "eslint . --quiet --fix", + "format:check": "prettier --cache --ignore-path ../../.prettierignore --check \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", + "format:fix": "prettier --cache --ignore-path ../../.prettierignore --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", + "test": "vitest run", + "test:watch": "vitest", "preview": "vite preview" }, "dependencies": { @@ -33,9 +38,11 @@ "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", "postcss": "^8.5.3", + "prettier": "^3.5.3", "tailwindcss": "^3.4.17", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", - "vite": "^6.3.5" + "vite": "^6.3.5", + "vitest": "^4.1.1" } } diff --git a/packages/frontend/src/components/ui/badge.tsx b/packages/frontend/src/components/ui/badge.tsx index 1917c24c..0c43f591 100644 --- a/packages/frontend/src/components/ui/badge.tsx +++ b/packages/frontend/src/components/ui/badge.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { Slot } from '@radix-ui/react-slot'; import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from '@/lib/utils'; +import { mergeClassNames } from '@/lib/utils'; const badgeVariants = cva( 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', @@ -30,7 +30,7 @@ function Badge({ }: React.ComponentProps<'span'> & VariantProps & { asChild?: boolean }) { const Comp = asChild ? Slot : 'span'; - return ; + return ; } export { Badge, badgeVariants }; diff --git a/packages/frontend/src/components/ui/button.tsx b/packages/frontend/src/components/ui/button.tsx index 29bc82f1..ff219016 100644 --- a/packages/frontend/src/components/ui/button.tsx +++ b/packages/frontend/src/components/ui/button.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { Slot } from '@radix-ui/react-slot'; import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from '@/lib/utils'; +import { mergeClassNames } from '@/lib/utils'; const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", @@ -44,7 +44,9 @@ function Button({ }) { const Comp = asChild ? Slot : 'button'; - return ; + return ( + + ); } export { Button, buttonVariants }; diff --git a/packages/frontend/src/components/ui/card.tsx b/packages/frontend/src/components/ui/card.tsx index 321e0114..51f01797 100644 --- a/packages/frontend/src/components/ui/card.tsx +++ b/packages/frontend/src/components/ui/card.tsx @@ -1,12 +1,15 @@ import * as React from 'react'; -import { cn } from '@/lib/utils'; +import { mergeClassNames } from '@/lib/utils'; function Card({ className, ...props }: React.ComponentProps<'div'>) { return (
); @@ -16,7 +19,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { return (
) { } function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { - return
; + return
; } function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { - return
; + return ( +
+ ); } function CardAction({ className, ...props }: React.ComponentProps<'div'>) { return (
); } function CardContent({ className, ...props }: React.ComponentProps<'div'>) { - return
; + return
; } function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { return ( -
+
); } diff --git a/packages/frontend/src/components/ui/input.tsx b/packages/frontend/src/components/ui/input.tsx index e8f37bad..438a5000 100644 --- a/packages/frontend/src/components/ui/input.tsx +++ b/packages/frontend/src/components/ui/input.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; -import { cn } from '@/lib/utils'; +import { mergeClassNames } from '@/lib/utils'; function Input({ className, type, ...props }: React.ComponentProps<'input'>) { return ( ) { return ( ) { return (
- +
); } function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) { - return ; + return ; } function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) { - return ; + return ( + + ); } function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) { return ( tr]:last:border-b-0', className)} + className={mergeClassNames('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', className)} {...props} /> ); @@ -32,7 +34,10 @@ function TableRow({ className, ...props }: React.ComponentProps<'tr'>) { return ( ); @@ -42,7 +47,7 @@ function TableHead({ className, ...props }: React.ComponentProps<'th'>) { return (
[role=checkbox]]:translate-y-[2px]', className, )} @@ -55,7 +60,10 @@ function TableCell({ className, ...props }: React.ComponentProps<'td'>) { return ( [role=checkbox]]:translate-y-[2px]', className)} + className={mergeClassNames( + 'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', + className, + )} {...props} /> ); @@ -63,7 +71,11 @@ function TableCell({ className, ...props }: React.ComponentProps<'td'>) { function TableCaption({ className, ...props }: React.ComponentProps<'caption'>) { return ( -
+ ); } diff --git a/packages/frontend/src/lib/utils.ts b/packages/frontend/src/lib/utils.ts index 9ad0df42..6e08ee63 100644 --- a/packages/frontend/src/lib/utils.ts +++ b/packages/frontend/src/lib/utils.ts @@ -1,6 +1,6 @@ import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; -export function cn(...inputs: ClassValue[]) { +export function mergeClassNames(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } From daf584f1bb59ff8900daa57eddab8a3284a86a62 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:44:43 +0000 Subject: [PATCH 06/38] Address code review feedback: scope CORS, named consts, rename cn, fix title, add frontend scripts Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/0812ae7b-89b6-4c45-af85-c62e3a618fa6 --- packages/frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 4145b881..a67f77fe 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -11,7 +11,7 @@ "lint:fix": "eslint . --quiet --fix", "format:check": "prettier --cache --ignore-path ../../.prettierignore --check \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", "format:fix": "prettier --cache --ignore-path ../../.prettierignore --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", - "test": "vitest run", + "test": "vitest run --passWithNoTests", "test:watch": "vitest", "preview": "vite preview" }, From a2ea0f988489ac7865fad1ea1a44492a3f34166f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:01:25 +0000 Subject: [PATCH 07/38] Implement remaining reviewer feedback: CSRF state, token fragment, fail-closed secret, limit validation, a11y, ESM fix Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/3a7aab8d-55c4-4541-b4ce-c9324af31b7e --- .../backend/src/auth/auth.controller.spec.ts | 81 ++++++++++++++++--- packages/backend/src/auth/auth.controller.ts | 34 +++++++- .../src/search/search.controller.spec.ts | 24 ++++++ .../backend/src/search/search.controller.ts | 12 ++- .../search/search.persistence.service.spec.ts | 27 +++++++ .../src/search/search.persistence.service.ts | 16 +++- .../shared/middleware/authMiddleware.spec.ts | 19 +++++ .../src/shared/middleware/authMiddleware.ts | 10 ++- .../src/shared/utils/session-token.spec.ts | 12 +++ .../backend/src/shared/utils/session-token.ts | 11 ++- packages/frontend/src/App.tsx | 8 +- .../frontend/src/components/LoginPage.tsx | 6 +- packages/frontend/tailwind.config.js | 3 +- 13 files changed, 232 insertions(+), 31 deletions(-) diff --git a/packages/backend/src/auth/auth.controller.spec.ts b/packages/backend/src/auth/auth.controller.spec.ts index e7ce4d24..f513af70 100644 --- a/packages/backend/src/auth/auth.controller.spec.ts +++ b/packages/backend/src/auth/auth.controller.spec.ts @@ -9,6 +9,9 @@ jest.mock('../shared/utils/session-token', () => ({ import { authController } from './auth.controller'; +const TEST_STATE = 'test-state-value'; +const STATE_COOKIE = `oauth_state=${TEST_STATE}`; + describe('authController', () => { const app = express(); app.use(express.json()); @@ -32,12 +35,16 @@ describe('authController', () => { }); describe('GET /slack', () => { - it('redirects to Slack OAuth URL with client_id and user_scope', async () => { + it('redirects to Slack OAuth URL with client_id, user_scope, and state param', async () => { const res = await request(app).get('/slack'); expect(res.status).toBe(302); expect(res.headers.location).toContain('slack.com/oauth/v2/authorize'); expect(res.headers.location).toContain('client_id=test-client-id'); expect(res.headers.location).toContain('user_scope=identity.basic'); + expect(res.headers.location).toContain('state='); + const cookies = res.headers['set-cookie'] as string[] | undefined; + expect(cookies).toBeDefined(); + expect(cookies?.some((c: string) => c.startsWith('oauth_state='))).toBe(true); }); it('returns 500 when SLACK_CLIENT_ID is not set', async () => { @@ -48,7 +55,7 @@ describe('authController', () => { }); describe('GET /slack/callback', () => { - it('redirects to frontend with token on successful OAuth', async () => { + it('redirects to frontend with token in hash on successful OAuth', async () => { (Axios.post as jest.Mock).mockResolvedValue({ data: { ok: true, authed_user: { id: 'U123', access_token: 'xoxp-token' } }, }); @@ -56,40 +63,76 @@ describe('authController', () => { data: { ok: true, user: { id: 'U123', name: 'alice' }, team: { domain: 'dabros2016', id: 'T123' } }, }); - const res = await request(app).get('/slack/callback').query({ code: 'valid-code' }); + const res = await request(app) + .get('/slack/callback') + .set('Cookie', STATE_COOKIE) + .query({ code: 'valid-code', state: TEST_STATE }); + + expect(res.status).toBe(302); + expect(res.headers.location).toContain('#token=mock-session-token'); + }); + it('redirects with auth_error=access_denied when state cookie is missing', async () => { + const res = await request(app).get('/slack/callback').query({ code: 'valid-code', state: TEST_STATE }); expect(res.status).toBe(302); - expect(res.headers.location).toContain('token=mock-session-token'); + expect(res.headers.location).toContain('auth_error=access_denied'); + }); + + it('redirects with auth_error=access_denied when state does not match cookie', async () => { + const res = await request(app) + .get('/slack/callback') + .set('Cookie', STATE_COOKIE) + .query({ code: 'valid-code', state: 'wrong-state' }); + expect(res.status).toBe(302); + expect(res.headers.location).toContain('auth_error=access_denied'); + }); + + it('redirects with auth_error=access_denied when state query param is missing', async () => { + const res = await request(app).get('/slack/callback').set('Cookie', STATE_COOKIE).query({ code: 'valid-code' }); + expect(res.status).toBe(302); + expect(res.headers.location).toContain('auth_error=access_denied'); }); it('redirects with auth_error=access_denied when error param is present', async () => { - const res = await request(app).get('/slack/callback').query({ error: 'access_denied' }); + const res = await request(app) + .get('/slack/callback') + .set('Cookie', STATE_COOKIE) + .query({ error: 'access_denied', state: TEST_STATE }); expect(res.status).toBe(302); expect(res.headers.location).toContain('auth_error=access_denied'); }); it('redirects with auth_error=access_denied when code is missing', async () => { - const res = await request(app).get('/slack/callback'); + const res = await request(app).get('/slack/callback').set('Cookie', STATE_COOKIE).query({ state: TEST_STATE }); expect(res.status).toBe(302); expect(res.headers.location).toContain('auth_error=access_denied'); }); it('returns 500 when SLACK_CLIENT_SECRET is not set', async () => { delete process.env.SLACK_CLIENT_SECRET; - const res = await request(app).get('/slack/callback').query({ code: 'some-code' }); + const res = await request(app) + .get('/slack/callback') + .set('Cookie', STATE_COOKIE) + .query({ code: 'some-code', state: TEST_STATE }); expect(res.status).toBe(500); }); it('returns 500 when SLACK_CLIENT_ID is not set', async () => { delete process.env.SLACK_CLIENT_ID; - const res = await request(app).get('/slack/callback').query({ code: 'some-code' }); + const res = await request(app) + .get('/slack/callback') + .set('Cookie', STATE_COOKIE) + .query({ code: 'some-code', state: TEST_STATE }); expect(res.status).toBe(500); }); it('redirects with auth_error=token_exchange_failed when Slack token response is not ok', async () => { (Axios.post as jest.Mock).mockResolvedValue({ data: { ok: false } }); - const res = await request(app).get('/slack/callback').query({ code: 'bad-code' }); + const res = await request(app) + .get('/slack/callback') + .set('Cookie', STATE_COOKIE) + .query({ code: 'bad-code', state: TEST_STATE }); expect(res.status).toBe(302); expect(res.headers.location).toContain('auth_error=token_exchange_failed'); }); @@ -97,7 +140,10 @@ describe('authController', () => { it('redirects with auth_error=token_exchange_failed when authed_user is missing', async () => { (Axios.post as jest.Mock).mockResolvedValue({ data: { ok: true } }); - const res = await request(app).get('/slack/callback').query({ code: 'bad-code' }); + const res = await request(app) + .get('/slack/callback') + .set('Cookie', STATE_COOKIE) + .query({ code: 'bad-code', state: TEST_STATE }); expect(res.status).toBe(302); expect(res.headers.location).toContain('auth_error=token_exchange_failed'); }); @@ -110,7 +156,10 @@ describe('authController', () => { data: { ok: true, user: { id: 'U999', name: 'bob' }, team: { domain: 'otherworkspace', id: 'T999' } }, }); - const res = await request(app).get('/slack/callback').query({ code: 'wrong-team-code' }); + const res = await request(app) + .get('/slack/callback') + .set('Cookie', STATE_COOKIE) + .query({ code: 'wrong-team-code', state: TEST_STATE }); expect(res.status).toBe(302); expect(res.headers.location).toContain('auth_error=unauthorized_workspace'); }); @@ -123,7 +172,10 @@ describe('authController', () => { data: { ok: false }, }); - const res = await request(app).get('/slack/callback').query({ code: 'bad-identity' }); + const res = await request(app) + .get('/slack/callback') + .set('Cookie', STATE_COOKIE) + .query({ code: 'bad-identity', state: TEST_STATE }); expect(res.status).toBe(302); expect(res.headers.location).toContain('auth_error=unauthorized_workspace'); }); @@ -131,7 +183,10 @@ describe('authController', () => { it('redirects with auth_error=server_error when an unexpected exception is thrown', async () => { (Axios.post as jest.Mock).mockRejectedValue(new Error('Network failure')); - const res = await request(app).get('/slack/callback').query({ code: 'throw-code' }); + const res = await request(app) + .get('/slack/callback') + .set('Cookie', STATE_COOKIE) + .query({ code: 'throw-code', state: TEST_STATE }); expect(res.status).toBe(302); expect(res.headers.location).toContain('auth_error=server_error'); }); diff --git a/packages/backend/src/auth/auth.controller.ts b/packages/backend/src/auth/auth.controller.ts index 09c01492..e1dcd479 100644 --- a/packages/backend/src/auth/auth.controller.ts +++ b/packages/backend/src/auth/auth.controller.ts @@ -1,4 +1,5 @@ -import type { Router } from 'express'; +import crypto from 'crypto'; +import type { Request, Router } from 'express'; import express from 'express'; import Axios from 'axios'; import { createSessionToken } from '../shared/utils/session-token'; @@ -12,6 +13,8 @@ const ALLOWED_TEAM_DOMAIN = 'dabros2016'; const SLACK_AUTH_URL = 'https://slack.com/oauth/v2/authorize'; const SLACK_TOKEN_URL = 'https://slack.com/api/oauth.v2.access'; const SLACK_IDENTITY_URL = 'https://slack.com/api/users.identity'; +const OAUTH_STATE_COOKIE = 'oauth_state'; +const OAUTH_STATE_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes interface SlackTokenResponse { ok: boolean; @@ -33,6 +36,14 @@ interface SlackIdentityResponse { }; } +function getCookieValue(req: Request, name: string): string | undefined { + const cookieHeader = req.headers.cookie; + if (!cookieHeader) return undefined; + const match = cookieHeader.split(';').find((c) => c.trim().startsWith(`${name}=`)); + if (!match) return undefined; + return match.split('=').slice(1).join('='); +} + authController.get('/slack', (_req, res) => { const clientId = process.env.SLACK_CLIENT_ID; const redirectUri = process.env.SLACK_REDIRECT_URI ?? 'http://localhost:3000/auth/slack/callback'; @@ -42,10 +53,19 @@ authController.get('/slack', (_req, res) => { return; } + const state = crypto.randomBytes(16).toString('hex'); + res.cookie(OAUTH_STATE_COOKIE, state, { + httpOnly: true, + maxAge: OAUTH_STATE_MAX_AGE_MS, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + }); + const params = new URLSearchParams({ client_id: clientId, user_scope: 'identity.basic', redirect_uri: redirectUri, + state, }); res.redirect(`${SLACK_AUTH_URL}?${params.toString()}`); @@ -55,7 +75,15 @@ authController.get('/slack/callback', (req, res) => { const frontendUrl = process.env.SEARCH_FRONTEND_URL ?? 'http://localhost:5173'; void (async () => { - const { code, error } = req.query; + const { code, error, state: stateFromQuery } = req.query; + const stateFromCookie = getCookieValue(req, OAUTH_STATE_COOKIE); + + res.clearCookie(OAUTH_STATE_COOKIE); + + if (!stateFromCookie || typeof stateFromQuery !== 'string' || stateFromQuery !== stateFromCookie) { + res.redirect(`${frontendUrl}?auth_error=access_denied`); + return; + } if (error || typeof code !== 'string') { res.redirect(`${frontendUrl}?auth_error=access_denied`); @@ -99,7 +127,7 @@ authController.get('/slack/callback', (req, res) => { } const sessionToken = createSessionToken(userId, teamDomain); - res.redirect(`${frontendUrl}?token=${sessionToken}`); + res.redirect(`${frontendUrl}#token=${sessionToken}`); })().catch((e: unknown) => { logError(authLogger, 'Slack OAuth callback failed', e, {}); res.redirect(`${frontendUrl}?auth_error=server_error`); diff --git a/packages/backend/src/search/search.controller.spec.ts b/packages/backend/src/search/search.controller.spec.ts index 2f54beec..fd55232b 100644 --- a/packages/backend/src/search/search.controller.spec.ts +++ b/packages/backend/src/search/search.controller.spec.ts @@ -49,6 +49,30 @@ describe('searchController', () => { }); }); + it('clamps limit to MAX_LIMIT (1000) when provided value exceeds it', async () => { + searchMessagesMock.mockResolvedValue([]); + + await request(app).get('/messages').query({ limit: '9999' }).expect(200); + + expect(searchMessagesMock).toHaveBeenCalledWith(expect.objectContaining({ limit: 1000 })); + }); + + it('passes undefined for limit when the value is not a valid positive integer (NaN)', async () => { + searchMessagesMock.mockResolvedValue([]); + + await request(app).get('/messages').query({ limit: 'abc' }).expect(200); + + expect(searchMessagesMock).toHaveBeenCalledWith(expect.objectContaining({ limit: undefined })); + }); + + it('passes undefined for limit when the value is zero or negative', async () => { + searchMessagesMock.mockResolvedValue([]); + + await request(app).get('/messages').query({ limit: '-5' }).expect(200); + + expect(searchMessagesMock).toHaveBeenCalledWith(expect.objectContaining({ limit: undefined })); + }); + it('returns 500 when search throws an error', async () => { searchMessagesMock.mockRejectedValue(new Error('DB failure')); diff --git a/packages/backend/src/search/search.controller.ts b/packages/backend/src/search/search.controller.ts index 42242a63..84e2cafc 100644 --- a/packages/backend/src/search/search.controller.ts +++ b/packages/backend/src/search/search.controller.ts @@ -9,15 +9,25 @@ export const searchController: Router = express.Router(); const searchPersistenceService = new SearchPersistenceService(); const searchLogger = logger.child({ module: 'SearchController' }); +const MAX_LIMIT = 1000; + searchController.get('/messages', (req, res) => { const { userName, channel, content, limit } = req.query; + let parsedLimit: number | undefined; + if (typeof limit === 'string') { + const parsed = parseInt(limit, 10); + if (Number.isFinite(parsed) && parsed > 0) { + parsedLimit = Math.min(parsed, MAX_LIMIT); + } + } + searchPersistenceService .searchMessages({ userName: typeof userName === 'string' ? userName : undefined, channel: typeof channel === 'string' ? channel : undefined, content: typeof content === 'string' ? content : undefined, - limit: typeof limit === 'string' ? parseInt(limit, 10) : undefined, + limit: parsedLimit, }) .then((messages) => res.status(200).json(messages)) .catch((e: unknown) => { diff --git a/packages/backend/src/search/search.persistence.service.spec.ts b/packages/backend/src/search/search.persistence.service.spec.ts index 3b22edec..7da7c486 100644 --- a/packages/backend/src/search/search.persistence.service.spec.ts +++ b/packages/backend/src/search/search.persistence.service.spec.ts @@ -73,6 +73,33 @@ describe('SearchPersistenceService', () => { expect(params[params.length - 1]).toBe(25); }); + it('clamps limit to MAX_LIMIT (500) when provided value exceeds it', async () => { + query.mockResolvedValue([]); + + await service.searchMessages({ limit: 9999 }); + + const [, params] = (query as jest.Mock).mock.calls[0] as [string, unknown[]]; + expect(params[params.length - 1]).toBe(500); + }); + + it('uses default limit when provided value is below MIN_LIMIT', async () => { + query.mockResolvedValue([]); + + await service.searchMessages({ limit: 0 }); + + const [, params] = (query as jest.Mock).mock.calls[0] as [string, unknown[]]; + expect(params[params.length - 1]).toBe(100); + }); + + it('uses default limit when provided value is NaN', async () => { + query.mockResolvedValue([]); + + await service.searchMessages({ limit: NaN }); + + const [, params] = (query as jest.Mock).mock.calls[0] as [string, unknown[]]; + expect(params[params.length - 1]).toBe(100); + }); + it('rethrows and logs errors from the database', async () => { const error = new Error('DB error'); query.mockRejectedValue(error); diff --git a/packages/backend/src/search/search.persistence.service.ts b/packages/backend/src/search/search.persistence.service.ts index 1fab4c15..e5ae3e91 100644 --- a/packages/backend/src/search/search.persistence.service.ts +++ b/packages/backend/src/search/search.persistence.service.ts @@ -14,8 +14,20 @@ export interface MessageSearchParams { export class SearchPersistenceService { private logger = logger.child({ module: 'SearchPersistenceService' }); + private static readonly DEFAULT_LIMIT = 100; + private static readonly MIN_LIMIT = 1; + private static readonly MAX_LIMIT = 500; + + private static resolveLimit(limit: number | undefined): number { + if (limit === undefined || !Number.isFinite(limit) || limit < SearchPersistenceService.MIN_LIMIT) { + return SearchPersistenceService.DEFAULT_LIMIT; + } + return Math.min(limit, SearchPersistenceService.MAX_LIMIT); + } + async searchMessages(params: MessageSearchParams): Promise { - const { userName, channel, content, limit = 100 } = params; + const { userName, channel, content } = params; + const effectiveLimit = SearchPersistenceService.resolveLimit(params.limit); const conditions: string[] = ["message.message != ''"]; const queryParams: (string | number)[] = []; @@ -46,7 +58,7 @@ export class SearchPersistenceService { LIMIT ? `; - queryParams.push(limit); + queryParams.push(effectiveLimit); return getRepository(Message) .query(query, queryParams) diff --git a/packages/backend/src/shared/middleware/authMiddleware.spec.ts b/packages/backend/src/shared/middleware/authMiddleware.spec.ts index 1fcc4507..3edda8ad 100644 --- a/packages/backend/src/shared/middleware/authMiddleware.spec.ts +++ b/packages/backend/src/shared/middleware/authMiddleware.spec.ts @@ -71,4 +71,23 @@ describe('authMiddleware', () => { expect(next).not.toHaveBeenCalled(); expect(res.status).toHaveBeenCalledWith(401); }); + + it('returns 401 when token verification throws (e.g. SEARCH_AUTH_SECRET not set)', () => { + const origNodeEnv = process.env.NODE_ENV; + delete process.env.SEARCH_AUTH_SECRET; + process.env.NODE_ENV = 'production'; + try { + const req = makeReq('Bearer sometoken.withsig'); + const res = makeRes(); + const next = jest.fn() as unknown as NextFunction; + + authMiddleware(req as Request, res as Response, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(401); + } finally { + process.env.SEARCH_AUTH_SECRET = 'test-secret'; + process.env.NODE_ENV = origNodeEnv ?? ''; + } + }); }); diff --git a/packages/backend/src/shared/middleware/authMiddleware.ts b/packages/backend/src/shared/middleware/authMiddleware.ts index 1de927d9..c84bf93c 100644 --- a/packages/backend/src/shared/middleware/authMiddleware.ts +++ b/packages/backend/src/shared/middleware/authMiddleware.ts @@ -12,7 +12,15 @@ export const authMiddleware = (req: Request, res: Response, next: NextFunction): } const token = authHeader.slice(7); - const session = verifySessionToken(token); + let session: ReturnType; + try { + session = verifySessionToken(token); + } catch { + authLogger.error('Session token verification failed — SEARCH_AUTH_SECRET may not be set'); + res.status(401).json({ error: 'Unauthorized' }); + return; + } + if (!session) { authLogger.warn('Invalid or expired session token'); res.status(401).json({ error: 'Unauthorized' }); diff --git a/packages/backend/src/shared/utils/session-token.spec.ts b/packages/backend/src/shared/utils/session-token.spec.ts index b5b37b36..4d849028 100644 --- a/packages/backend/src/shared/utils/session-token.spec.ts +++ b/packages/backend/src/shared/utils/session-token.spec.ts @@ -36,6 +36,18 @@ describe('session-token', () => { expect(decoded.exp).toBeGreaterThan(before); expect(decoded.exp).toBeGreaterThan(after); }); + + it('throws when SEARCH_AUTH_SECRET is not set in a non-test environment', () => { + const origNodeEnv = process.env.NODE_ENV; + delete process.env.SEARCH_AUTH_SECRET; + process.env.NODE_ENV = 'production'; + try { + expect(() => createSessionToken('U1', 'team')).toThrow(); + } finally { + process.env.SEARCH_AUTH_SECRET = 'test-secret'; + process.env.NODE_ENV = origNodeEnv ?? ''; + } + }); }); describe('verifySessionToken', () => { diff --git a/packages/backend/src/shared/utils/session-token.ts b/packages/backend/src/shared/utils/session-token.ts index b59ff55a..ca8de1c8 100644 --- a/packages/backend/src/shared/utils/session-token.ts +++ b/packages/backend/src/shared/utils/session-token.ts @@ -1,14 +1,17 @@ import crypto from 'crypto'; -import { logger } from '../logger/logger'; const TOKEN_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours -const secretLogger = logger.child({ module: 'SessionToken' }); function getSecret(): string { const secret = process.env.SEARCH_AUTH_SECRET; if (!secret) { - secretLogger.warn('SEARCH_AUTH_SECRET is not set — session tokens are insecure. Set this variable in production.'); - return 'insecure-default-secret'; + if (process.env.NODE_ENV === 'test') { + return 'insecure-test-secret'; + } + throw new Error( + 'SEARCH_AUTH_SECRET environment variable is not set. ' + + 'Session tokens cannot be created or verified without a secret.', + ); } return secret; } diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 3eb81d64..f4f12202 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -44,11 +44,13 @@ export default function App() { useEffect(() => { const urlParams = new URLSearchParams(window.location.search); - const tokenFromUrl = urlParams.get('token'); const errorFromUrl = urlParams.get('auth_error') ?? undefined; - if (tokenFromUrl) { - localStorage.setItem(AUTH_TOKEN_KEY, tokenFromUrl); + const hashParams = new URLSearchParams(window.location.hash.slice(1)); + const tokenFromHash = hashParams.get('token'); + + if (tokenFromHash) { + localStorage.setItem(AUTH_TOKEN_KEY, tokenFromHash); window.history.replaceState({}, '', window.location.pathname); setIsAuthenticated(true); } else if (getStoredToken()) { diff --git a/packages/frontend/src/components/LoginPage.tsx b/packages/frontend/src/components/LoginPage.tsx index 933fb0cb..57bd3958 100644 --- a/packages/frontend/src/components/LoginPage.tsx +++ b/packages/frontend/src/components/LoginPage.tsx @@ -29,9 +29,9 @@ export function LoginPage({ authError }: LoginPageProps) { {errorMessage &&

{errorMessage}

} - - - +
diff --git a/packages/frontend/tailwind.config.js b/packages/frontend/tailwind.config.js index 13df8673..521bae3f 100644 --- a/packages/frontend/tailwind.config.js +++ b/packages/frontend/tailwind.config.js @@ -1,4 +1,5 @@ /** @type {import('tailwindcss').Config} */ +import tailwindAnimate from 'tailwindcss-animate'; export default { darkMode: ['class'], content: ['./index.html', './src/**/*.{ts,tsx,js,jsx}'], @@ -53,5 +54,5 @@ export default { }, }, }, - plugins: [require('tailwindcss-animate')], + plugins: [tailwindAnimate], }; From f4a56a39935ade904796c152b55f54a138c96551 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:10:07 +0000 Subject: [PATCH 08/38] Fix CI failures: scope root lint to backend, fix eslint.config.js formatting Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/6c5cbbbf-7d87-45e8-949f-1d5d9aa7e2de --- eslint.config.js | 8 +------- package.json | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 6d237c25..6f82ce4d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,13 +5,7 @@ const tseslint = require('typescript-eslint'); module.exports = tseslint.config( { - ignores: [ - '**/node_modules/**', - '**/dist/**', - '**/coverage/**', - '**/*.d.ts', - 'packages/backend/scripts/*.js', - ], + ignores: ['**/node_modules/**', '**/dist/**', '**/coverage/**', '**/*.d.ts', 'packages/backend/scripts/*.js'], }, js.configs.recommended, ...tseslint.configs.recommended, diff --git a/package.json b/package.json index 83757776..bf24da07 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "test:backend": "npm run test -w @mocker/backend", "test:coverage": "npm run test:coverage -w @mocker/backend", "dev:frontend": "npm run dev -w @mocker/frontend", - "lint": "eslint --cache --cache-location node_modules/.cache/eslint/root/.eslintcache packages/**/*.ts", - "lint:fix": "eslint --cache --cache-location node_modules/.cache/eslint/root/.eslintcache packages/**/*.ts --quiet --fix", + "lint": "eslint --cache --cache-location node_modules/.cache/eslint/root/.eslintcache packages/backend/**/*.ts", + "lint:fix": "eslint --cache --cache-location node_modules/.cache/eslint/root/.eslintcache packages/backend/**/*.ts --quiet --fix", "format:check": "prettier --cache --check \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", "format:fix": "prettier --cache --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"" }, From 632f5386005ec2194f8f2854f032b0311b908aca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:13:57 +0000 Subject: [PATCH 09/38] Use literal values for rate limit consts; add frontend CI jobs with differentiated names Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/fb0496f4-3ba1-43d3-aeaa-af154422df83 --- .github/workflows/ci.yml | 44 +++++++++++++++++++++++++++++++---- packages/backend/src/index.ts | 4 ++-- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 585989d3..fd019e0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: retention-days: 1 lint: - name: Lint + name: Backend Lint needs: setup runs-on: ubuntu-latest permissions: @@ -40,6 +40,18 @@ jobs: - uses: ./.github/actions/setup - run: npm run lint + lint-frontend: + name: Frontend Lint + needs: setup + runs-on: ubuntu-latest + permissions: + contents: read + actions: read + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - run: npm run lint -w @mocker/frontend + format-check: name: Format Check needs: setup @@ -53,7 +65,7 @@ jobs: - run: npm run format:check build: - name: Build + name: Backend Build needs: setup runs-on: ubuntu-latest permissions: @@ -62,10 +74,22 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup - - run: npm run build + - run: npm run build:backend + + build-frontend: + name: Frontend Build + needs: setup + runs-on: ubuntu-latest + permissions: + contents: read + actions: read + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - run: npm run build:frontend test: - name: Test & Coverage + name: Backend Test & Coverage needs: setup runs-on: ubuntu-latest permissions: @@ -77,6 +101,18 @@ jobs: - name: Enforce 80% coverage run: npm run test:coverage + test-frontend: + name: Frontend Test + needs: setup + runs-on: ubuntu-latest + permissions: + contents: read + actions: read + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - run: npm run test -w @mocker/frontend + docker-build: name: Docker Build runs-on: ubuntu-latest diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index a5fecdb7..ac90856c 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -55,8 +55,8 @@ app.use( }, }), ); -const AUTH_RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes -const SEARCH_RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute +const AUTH_RATE_LIMIT_WINDOW_MS = 900000; // 15 minutes +const SEARCH_RATE_LIMIT_WINDOW_MS = 60000; // 1 minute const authRateLimit = rateLimit({ windowMs: AUTH_RATE_LIMIT_WINDOW_MS, From 3c69c3c534157724644b050cf930d4a944a32018 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:17:57 +0000 Subject: [PATCH 10/38] Fix frontend lint script to mirror backend pattern (--cache, explicit globs) Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/d4bc8dab-d238-4697-b727-abfa044d2b08 --- packages/frontend/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/package.json b/packages/frontend/package.json index a67f77fe..ea9a4120 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -7,8 +7,8 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "lint": "eslint .", - "lint:fix": "eslint . --quiet --fix", + "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/frontend/.eslintcache './**/*.{ts,tsx}'", + "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/frontend/.eslintcache './**/*.{ts,tsx}' --quiet --fix", "format:check": "prettier --cache --ignore-path ../../.prettierignore --check \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", "format:fix": "prettier --cache --ignore-path ../../.prettierignore --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", "test": "vitest run --passWithNoTests", From fe4366a1451e7e29f6b442c1768ac5f406446cda Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:27:29 +0000 Subject: [PATCH 11/38] Fix frontend lint CI: include workspace node_modules in tarball; consolidate CORS env vars Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/3774db63-89f4-4998-82d7-7feab0af55d8 --- .github/workflows/ci.yml | 2 +- packages/backend/src/index.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd019e0a..2518cfd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: node-version: '20' cache: 'npm' - run: npm ci - - run: tar -czf node-modules.tar.gz node_modules + - run: tar -czf node-modules.tar.gz node_modules packages/*/node_modules - uses: actions/upload-artifact@v4 with: name: node-modules diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index ac90856c..9bfd25b8 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -38,7 +38,9 @@ import { authMiddleware } from './shared/middleware/authMiddleware'; const app: Application = express(); const PORT = process.env.PORT || 3000; -const searchCors = cors({ origin: process.env.SEARCH_UI_ORIGIN || 'http://localhost:5173' }); +const searchCors = cors({ + origin: process.env.SEARCH_FRONTEND_URL || process.env.SEARCH_UI_ORIGIN || 'http://localhost:5173', +}); app.use( bodyParser.urlencoded({ From e637c4a4dd9f37ee5192913e3d0a7e38bdbd6289 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:33:39 +0000 Subject: [PATCH 12/38] Add 80% coverage enforcement to Frontend Test CI job (vitest coverage-v8, test:coverage script) Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/75f1a10e-c087-45b7-b94b-a28ce9207f66 --- .github/workflows/ci.yml | 3 +- package-lock.json | 87 +++++++++++++++++++++++++++++--- packages/frontend/package.json | 2 + packages/frontend/vite.config.ts | 11 ++++ 4 files changed, 95 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2518cfd1..89858ca2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,7 +111,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup - - run: npm run test -w @mocker/frontend + - name: Enforce 80% coverage + run: npm run test:coverage -w @mocker/frontend docker-build: name: Docker Build diff --git a/package-lock.json b/package-lock.json index ddd08c46..7ed89008 100644 --- a/package-lock.json +++ b/package-lock.json @@ -244,13 +244,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -565,9 +565,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { @@ -3398,6 +3398,47 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.1.tgz", + "integrity": "sha512-nZ4RWwGCoGOQRMmU/Q9wlUY540RVRxJZ9lxFsFfy0QV7Zmo5VVBhB6Sl9Xa0KIp2iIs3zWfPlo9LcY1iqbpzCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.1", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.1", + "vitest": "4.1.1" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8/node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@vitest/expect": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.1.tgz", @@ -3831,6 +3872,25 @@ "node": ">=0.10.0" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -9977,6 +10037,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -15815,6 +15887,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", + "@vitest/coverage-v8": "^4.1.1", "autoprefixer": "^10.4.21", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", diff --git a/packages/frontend/package.json b/packages/frontend/package.json index ea9a4120..787be7ab 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -12,6 +12,7 @@ "format:check": "prettier --cache --ignore-path ../../.prettierignore --check \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", "format:fix": "prettier --cache --ignore-path ../../.prettierignore --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", "test": "vitest run --passWithNoTests", + "test:coverage": "vitest run --coverage --passWithNoTests", "test:watch": "vitest", "preview": "vite preview" }, @@ -32,6 +33,7 @@ "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", + "@vitest/coverage-v8": "^4.1.1", "autoprefixer": "^10.4.21", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 95d423e2..f583d2cf 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -9,4 +9,15 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), }, }, + test: { + coverage: { + provider: 'v8', + thresholds: { + branches: 80, + functions: 80, + lines: 80, + statements: 80, + }, + }, + }, }); From 02bb78fb67ff21d98efc6405b338367b2add642c Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Mon, 23 Mar 2026 16:41:31 -0400 Subject: [PATCH 13/38] Updated steps in ci.yml --- .github/workflows/ci.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89858ca2..13faafa0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ permissions: jobs: setup: - name: Install Dependencies + name: Install Dependencies (All) runs-on: ubuntu-latest permissions: contents: read @@ -29,7 +29,7 @@ jobs: retention-days: 1 lint: - name: Backend Lint + name: Lint (Back End) needs: setup runs-on: ubuntu-latest permissions: @@ -41,7 +41,7 @@ jobs: - run: npm run lint lint-frontend: - name: Frontend Lint + name: Lint (Front End) needs: setup runs-on: ubuntu-latest permissions: @@ -53,7 +53,7 @@ jobs: - run: npm run lint -w @mocker/frontend format-check: - name: Format Check + name: Format Check (All) needs: setup runs-on: ubuntu-latest permissions: @@ -65,7 +65,7 @@ jobs: - run: npm run format:check build: - name: Backend Build + name: Build (Back End) needs: setup runs-on: ubuntu-latest permissions: @@ -77,7 +77,7 @@ jobs: - run: npm run build:backend build-frontend: - name: Frontend Build + name: Build (Front End) needs: setup runs-on: ubuntu-latest permissions: @@ -89,7 +89,7 @@ jobs: - run: npm run build:frontend test: - name: Backend Test & Coverage + name: Test & Coverage (Back End) needs: setup runs-on: ubuntu-latest permissions: @@ -102,7 +102,7 @@ jobs: run: npm run test:coverage test-frontend: - name: Frontend Test + name: Test & Coverage (Front End) needs: setup runs-on: ubuntu-latest permissions: @@ -115,7 +115,7 @@ jobs: run: npm run test:coverage -w @mocker/frontend docker-build: - name: Docker Build + name: Docker Build (Back End) runs-on: ubuntu-latest permissions: contents: read From b803ca6645f29e62f170b32dafc6cdf89d9eb1e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:46:29 +0000 Subject: [PATCH 14/38] Upgrade React ESLint plugins to latest, move to root with npm overrides for ESLint v10 Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/02364452-14a2-412f-bb0b-6009071e1559 --- package-lock.json | 490 ++++------------------------- package.json | 7 + packages/frontend/eslint.config.js | 4 +- packages/frontend/package.json | 5 - 4 files changed, 74 insertions(+), 432 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7ed89008..0bcff7f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,8 @@ "@typescript-eslint/parser": "^8.57.1", "eslint": "^10.1.0", "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", "husky": "^9.1.7", "lint-staged": "^16.4.0", @@ -1141,108 +1143,6 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/js": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", @@ -3792,13 +3692,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, "node_modules/arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -6088,14 +5981,34 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "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" + } + }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.26", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", - "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "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": ">=8.40" + "eslint": "^9 || ^10" } }, "node_modules/eslint-scope": { @@ -7503,6 +7416,23 @@ "node": ">= 0.4" } }, + "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/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -7627,23 +7557,6 @@ "dev": true, "license": "ISC" }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/import-lazy": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", @@ -9611,19 +9524,6 @@ "dev": true, "license": "MIT" }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -9889,13 +9789,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -11070,19 +10963,6 @@ "semver": "bin/semver" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -12110,16 +11990,6 @@ "node": ">=8" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -15821,6 +15691,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "devOptional": true, + "license": "MIT", + "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" + } + }, "packages/backend": { "name": "@mocker/backend", "version": "1.0.0", @@ -15883,16 +15776,11 @@ "tailwindcss-animate": "^1.0.7" }, "devDependencies": { - "@eslint/js": "^9.25.0", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", "@vitest/coverage-v8": "^4.1.1", "autoprefixer": "^10.4.21", - "eslint": "^9.25.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^16.0.0", "postcss": "^8.5.3", "prettier": "^3.5.3", "tailwindcss": "^3.4.17", @@ -15902,252 +15790,6 @@ "vitest": "^4.1.1" } }, - "packages/frontend/node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "packages/frontend/node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "packages/frontend/node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "packages/frontend/node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "packages/frontend/node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "packages/frontend/node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "packages/frontend/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "packages/frontend/node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "packages/frontend/node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "packages/frontend/node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "packages/frontend/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "packages/frontend/node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "packages/frontend/node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/frontend/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" - } - }, - "packages/frontend/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "packages/frontend/node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", diff --git a/package.json b/package.json index bf24da07..5277693e 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,8 @@ "@typescript-eslint/parser": "^8.57.1", "eslint": "^10.1.0", "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", "husky": "^9.1.7", "lint-staged": "^16.4.0", @@ -39,6 +41,11 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.57.1" }, + "overrides": { + "eslint-plugin-react-hooks": { + "eslint": "$eslint" + } + }, "lint-staged": { "packages/backend/**/*.{ts,js}": [ "eslint --quiet --fix", diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js index ea7d18ef..32007c85 100644 --- a/packages/frontend/eslint.config.js +++ b/packages/frontend/eslint.config.js @@ -7,18 +7,16 @@ import tseslint from 'typescript-eslint'; export default tseslint.config( { ignores: ['dist', 'coverage'] }, { - extends: [js.configs.recommended, ...tseslint.configs.recommended], + extends: [js.configs.recommended, ...tseslint.configs.recommended, reactHooks.configs.flat.recommended], files: ['**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { - 'react-hooks': reactHooks, 'react-refresh': reactRefresh, }, rules: { - ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], }, }, diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 787be7ab..b98c896c 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -29,16 +29,11 @@ "tailwindcss-animate": "^1.0.7" }, "devDependencies": { - "@eslint/js": "^9.25.0", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", "@vitest/coverage-v8": "^4.1.1", "autoprefixer": "^10.4.21", - "eslint": "^9.25.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^16.0.0", "postcss": "^8.5.3", "prettier": "^3.5.3", "tailwindcss": "^3.4.17", From e3db7fceede2001edcf4d2654de76c4cd933f6bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:58:38 +0000 Subject: [PATCH 15/38] Sync frontend TypeScript to ^5.9.3, simplify CI tarball Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/6c328c22-57d3-4c37-a7d5-2feb5020305d --- .github/workflows/ci.yml | 2 +- package-lock.json | 16 +--------------- packages/frontend/package.json | 2 +- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13faafa0..a92790fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: node-version: '20' cache: 'npm' - run: npm ci - - run: tar -czf node-modules.tar.gz node_modules packages/*/node_modules + - run: tar -czf node-modules.tar.gz node_modules - uses: actions/upload-artifact@v4 with: name: node-modules diff --git a/package-lock.json b/package-lock.json index 0bcff7f4..8b866740 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15784,25 +15784,11 @@ "postcss": "^8.5.3", "prettier": "^3.5.3", "tailwindcss": "^3.4.17", - "typescript": "~5.8.3", + "typescript": "^5.9.3", "typescript-eslint": "^8.30.1", "vite": "^6.3.5", "vitest": "^4.1.1" } - }, - "packages/frontend/node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } } } } diff --git a/packages/frontend/package.json b/packages/frontend/package.json index b98c896c..da2d62ae 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -37,7 +37,7 @@ "postcss": "^8.5.3", "prettier": "^3.5.3", "tailwindcss": "^3.4.17", - "typescript": "~5.8.3", + "typescript": "^5.9.3", "typescript-eslint": "^8.30.1", "vite": "^6.3.5", "vitest": "^4.1.1" From 23d6ceef321a50d3ba8b68ddf3476836903c6f3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:09:51 +0000 Subject: [PATCH 16/38] Move Slack interfaces to auth.model.ts; fix root lint scripts to cover both workspaces Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/b172869d-6763-43fd-8434-72edc6626bd8 --- package.json | 6 ++++-- packages/backend/src/auth/auth.controller.ts | 21 +------------------- packages/backend/src/auth/auth.model.ts | 19 ++++++++++++++++++ 3 files changed, 24 insertions(+), 22 deletions(-) create mode 100644 packages/backend/src/auth/auth.model.ts diff --git a/package.json b/package.json index 5277693e..02fbd59b 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,10 @@ "test:backend": "npm run test -w @mocker/backend", "test:coverage": "npm run test:coverage -w @mocker/backend", "dev:frontend": "npm run dev -w @mocker/frontend", - "lint": "eslint --cache --cache-location node_modules/.cache/eslint/root/.eslintcache packages/backend/**/*.ts", - "lint:fix": "eslint --cache --cache-location node_modules/.cache/eslint/root/.eslintcache packages/backend/**/*.ts --quiet --fix", + "lint": "npm run lint -w @mocker/backend && npm run lint -w @mocker/frontend", + "lint:fix": "npm run lint:fix -w @mocker/backend && npm run lint:fix -w @mocker/frontend", + "lint:backend": "npm run lint -w @mocker/backend", + "lint:frontend": "npm run lint -w @mocker/frontend", "format:check": "prettier --cache --check \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", "format:fix": "prettier --cache --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"" }, diff --git a/packages/backend/src/auth/auth.controller.ts b/packages/backend/src/auth/auth.controller.ts index e1dcd479..105fd4ee 100644 --- a/packages/backend/src/auth/auth.controller.ts +++ b/packages/backend/src/auth/auth.controller.ts @@ -5,6 +5,7 @@ import Axios from 'axios'; import { createSessionToken } from '../shared/utils/session-token'; import { logError } from '../shared/logger/error-logging'; import { logger } from '../shared/logger/logger'; +import type { SlackIdentityResponse, SlackTokenResponse } from './auth.model'; export const authController: Router = express.Router(); const authLogger = logger.child({ module: 'AuthController' }); @@ -16,26 +17,6 @@ const SLACK_IDENTITY_URL = 'https://slack.com/api/users.identity'; const OAUTH_STATE_COOKIE = 'oauth_state'; const OAUTH_STATE_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes -interface SlackTokenResponse { - ok: boolean; - authed_user?: { - id: string; - access_token: string; - }; -} - -interface SlackIdentityResponse { - ok: boolean; - user?: { - id: string; - name: string; - }; - team?: { - domain: string; - id: string; - }; -} - function getCookieValue(req: Request, name: string): string | undefined { const cookieHeader = req.headers.cookie; if (!cookieHeader) return undefined; diff --git a/packages/backend/src/auth/auth.model.ts b/packages/backend/src/auth/auth.model.ts new file mode 100644 index 00000000..700c2c92 --- /dev/null +++ b/packages/backend/src/auth/auth.model.ts @@ -0,0 +1,19 @@ +export interface SlackTokenResponse { + ok: boolean; + authed_user?: { + id: string; + access_token: string; + }; +} + +export interface SlackIdentityResponse { + ok: boolean; + user?: { + id: string; + name: string; + }; + team?: { + domain: string; + id: string; + }; +} From 0edbad4d406cf5f4592f255eac36e0ab1d96306d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:56:50 +0000 Subject: [PATCH 17/38] =?UTF-8?q?Add=20frontend=20unit=20tests:=20LoginPag?= =?UTF-8?q?e=20(7=20tests)=20and=20App=20(13=20tests)=20=E2=80=94=2080%+?= =?UTF-8?q?=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/4b2dcd3d-7ddc-4413-9a01-0bca38f84e22 --- package-lock.json | 798 ++++++++++++++++++ packages/frontend/package.json | 3 + packages/frontend/src/App.spec.tsx | 164 ++++ .../src/components/LoginPage.spec.tsx | 43 + packages/frontend/src/test/setup.ts | 1 + packages/frontend/vite.config.ts | 3 + 6 files changed, 1012 insertions(+) create mode 100644 packages/frontend/src/App.spec.tsx create mode 100644 packages/frontend/src/components/LoginPage.spec.tsx create mode 100644 packages/frontend/src/test/setup.ts diff --git a/package-lock.json b/package-lock.json index 8b866740..271dc193 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,13 @@ "typescript-eslint": "^8.57.1" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -39,6 +46,67 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.4.tgz", + "integrity": "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", @@ -532,6 +600,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -587,6 +665,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, "node_modules/@colors/colors": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", @@ -620,6 +711,146 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@dabh/diagnostics": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", @@ -1188,6 +1419,24 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@google/genai": { "version": "1.45.0", "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.45.0.tgz", @@ -2604,6 +2853,120 @@ "dev": true, "license": "MIT" }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@textlint/ast-node-types": { "version": "13.4.1", "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-13.4.1.tgz", @@ -2638,6 +3001,14 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3692,6 +4063,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -4102,6 +4483,16 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -5402,6 +5793,27 @@ "node": ">=4" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -5430,6 +5842,58 @@ "node": ">= 12" } }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/dayjs": { "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", @@ -5567,6 +6031,16 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -5630,6 +6104,14 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dot-prop": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", @@ -5765,6 +6247,19 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -7433,6 +7928,19 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -7597,6 +8105,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -7893,6 +8411,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-redirect": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", @@ -9524,6 +10049,95 @@ "dev": true, "license": "MIT" }, + "node_modules/jsdom": { + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz", + "integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@asamuzakjp/dom-selector": "^7.0.3", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.24.5", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -9920,6 +10534,17 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -10007,6 +10632,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -10109,6 +10741,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -10963,6 +11605,19 @@ "semver": "bin/semver" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -11846,6 +12501,20 @@ "node": ">=0.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", @@ -11947,6 +12616,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -12195,6 +12874,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -13061,6 +13753,19 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -13198,6 +13903,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", @@ -13588,6 +14300,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", + "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.27" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", + "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", + "dev": true, + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -13688,6 +14420,19 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -14111,6 +14856,16 @@ "dev": true, "license": "MIT" }, + "node_modules/undici": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz", + "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -15157,6 +15912,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -15192,6 +15960,16 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -15586,6 +16364,23 @@ "node": ">=4" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -15776,11 +16571,14 @@ "tailwindcss-animate": "^1.0.7" }, "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", "@vitest/coverage-v8": "^4.1.1", "autoprefixer": "^10.4.21", + "jsdom": "^29.0.1", "postcss": "^8.5.3", "prettier": "^3.5.3", "tailwindcss": "^3.4.17", diff --git a/packages/frontend/package.json b/packages/frontend/package.json index da2d62ae..8a0efb25 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -29,11 +29,14 @@ "tailwindcss-animate": "^1.0.7" }, "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", "@vitest/coverage-v8": "^4.1.1", "autoprefixer": "^10.4.21", + "jsdom": "^29.0.1", "postcss": "^8.5.3", "prettier": "^3.5.3", "tailwindcss": "^3.4.17", diff --git a/packages/frontend/src/App.spec.tsx b/packages/frontend/src/App.spec.tsx new file mode 100644 index 00000000..81d45b35 --- /dev/null +++ b/packages/frontend/src/App.spec.tsx @@ -0,0 +1,164 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import App from '@/App'; + +const mockFetch = vi.fn(); +global.fetch = mockFetch; + +beforeEach(() => { + localStorage.clear(); + mockFetch.mockReset(); + window.history.replaceState({}, '', '/'); +}); + +describe('App – unauthenticated state', () => { + it('renders the LoginPage when no token is present', () => { + render(); + expect(screen.getByRole('link', { name: /sign in with slack/i })).toBeInTheDocument(); + }); + + it('reads an auth_error from the URL and passes it to LoginPage', () => { + window.history.replaceState({}, '', '/?auth_error=unauthorized_workspace'); + render(); + expect(screen.getByText(/only members of the dabros2016\.slack\.com workspace/i)).toBeInTheDocument(); + }); +}); + +describe('App – token in URL hash', () => { + it('stores the token from the hash, clears the hash, and shows the search UI', () => { + window.history.replaceState({}, '', '/#token=test-token-123'); + render(); + expect(screen.getByText(/message search/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/user name/i)).toBeInTheDocument(); + expect(localStorage.getItem('auth_token')).toBe('test-token-123'); + expect(window.location.hash).toBe(''); + }); +}); + +describe('App – authenticated state', () => { + beforeEach(() => { + localStorage.setItem('auth_token', 'stored-token'); + }); + + it('shows the search UI when a token is already stored', () => { + render(); + expect(screen.getByLabelText(/user name/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/channel/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/message content/i)).toBeInTheDocument(); + }); + + it('clears the token and returns to LoginPage on Sign out click', () => { + render(); + fireEvent.click(screen.getByRole('button', { name: /sign out/i })); + expect(localStorage.getItem('auth_token')).toBeNull(); + expect(screen.getByRole('link', { name: /sign in with slack/i })).toBeInTheDocument(); + }); + + it('shows active filter badges when inputs have values', () => { + render(); + fireEvent.change(screen.getByLabelText(/user name/i), { target: { value: 'alice' } }); + fireEvent.change(screen.getByLabelText(/channel/i), { target: { value: 'general' } }); + fireEvent.change(screen.getByLabelText(/message content/i), { target: { value: 'hello' } }); + expect(screen.getByText(/user: alice/i)).toBeInTheDocument(); + expect(screen.getByText(/channel: general/i)).toBeInTheDocument(); + expect(screen.getByText(/content: hello/i)).toBeInTheDocument(); + }); + + it('triggers search on Enter keydown in an input', async () => { + mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: async () => [] }); + render(); + fireEvent.keyDown(screen.getByLabelText(/user name/i), { key: 'Enter' }); + await waitFor(() => expect(screen.getByText(/no messages found/i)).toBeInTheDocument()); + }); + + it('does not trigger search on a non-Enter keydown', async () => { + render(); + fireEvent.keyDown(screen.getByLabelText(/user name/i), { key: 'a' }); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('calls the search API and displays a single result', async () => { + const messages = [ + { + id: 1, + message: 'Hello world', + channel: 'general', + teamId: 'T1', + createdAt: '2024-01-01T00:00:00.000Z', + name: 'alice', + slackId: 'U1', + }, + ]; + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => messages, + }); + + render(); + fireEvent.click(screen.getByRole('button', { name: /^search$/i })); + + await waitFor(() => expect(screen.getByText('Hello world')).toBeInTheDocument()); + expect(screen.getByText('#general')).toBeInTheDocument(); + expect(screen.getByText('alice')).toBeInTheDocument(); + expect(screen.getByText(/found 1 message$/i)).toBeInTheDocument(); + }); + + it('displays plural "messages" for more than one result', async () => { + const messages = [ + { + id: 1, + message: 'A', + channel: 'c', + teamId: 'T', + createdAt: '2024-01-01T00:00:00.000Z', + name: 'u', + slackId: 'U1', + }, + { + id: 2, + message: 'B', + channel: 'c', + teamId: 'T', + createdAt: '2024-01-01T00:00:00.000Z', + name: 'v', + slackId: 'U2', + }, + ]; + mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: async () => messages }); + render(); + fireEvent.click(screen.getByRole('button', { name: /^search$/i })); + await waitFor(() => expect(screen.getByText(/found 2 messages/i)).toBeInTheDocument()); + }); + + it('shows "no messages found" when search returns an empty array', async () => { + mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: async () => [] }); + render(); + fireEvent.click(screen.getByRole('button', { name: /^search$/i })); + await waitFor(() => + expect(screen.getByText(/no messages found matching your search criteria/i)).toBeInTheDocument(), + ); + }); + + it('shows an error when the search API returns a non-ok response', async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + }); + + render(); + fireEvent.click(screen.getByRole('button', { name: /^search$/i })); + + await waitFor(() => expect(screen.getByText(/search failed: internal server error/i)).toBeInTheDocument()); + }); + + it('logs out when the API returns 401', async () => { + mockFetch.mockResolvedValueOnce({ ok: false, status: 401 }); + + render(); + fireEvent.click(screen.getByRole('button', { name: /^search$/i })); + + await waitFor(() => expect(screen.getByRole('link', { name: /sign in with slack/i })).toBeInTheDocument()); + expect(localStorage.getItem('auth_token')).toBeNull(); + }); +}); diff --git a/packages/frontend/src/components/LoginPage.spec.tsx b/packages/frontend/src/components/LoginPage.spec.tsx new file mode 100644 index 00000000..51bb6863 --- /dev/null +++ b/packages/frontend/src/components/LoginPage.spec.tsx @@ -0,0 +1,43 @@ +import { render, screen } from '@testing-library/react'; +import { LoginPage } from '@/components/LoginPage'; + +describe('LoginPage', () => { + it('renders the sign-in button linking to the auth endpoint', () => { + render(); + const link = screen.getByRole('link', { name: /sign in with slack/i }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', expect.stringContaining('/auth/slack')); + }); + + it('shows no error message when authError is not provided', () => { + render(); + expect(screen.queryByText(/access was denied/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/authentication failed/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/unexpected error/i)).not.toBeInTheDocument(); + }); + + it('shows a workspace-specific error when authError is unauthorized_workspace', () => { + render(); + expect(screen.getByText(/only members of the dabros2016\.slack\.com workspace/i)).toBeInTheDocument(); + }); + + it('shows a generic error for an unrecognised authError code', () => { + render(); + expect(screen.getByText(/authentication failed/i)).toBeInTheDocument(); + }); + + it('shows an access-denied message for the access_denied error code', () => { + render(); + expect(screen.getByText(/access was denied/i)).toBeInTheDocument(); + }); + + it('shows a token-exchange message for the token_exchange_failed error code', () => { + render(); + expect(screen.getByText(/authentication failed\. please try again\./i)).toBeInTheDocument(); + }); + + it('shows a server-error message for the server_error error code', () => { + render(); + expect(screen.getByText(/an unexpected error occurred/i)).toBeInTheDocument(); + }); +}); diff --git a/packages/frontend/src/test/setup.ts b/packages/frontend/src/test/setup.ts new file mode 100644 index 00000000..7b0828bf --- /dev/null +++ b/packages/frontend/src/test/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index f583d2cf..9b15dad6 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -10,6 +10,9 @@ export default defineConfig({ }, }, test: { + environment: 'jsdom', + globals: true, + setupFiles: ['./src/test/setup.ts'], coverage: { provider: 'v8', thresholds: { From 3c504a8da9c9a987d18c487d2c34aae3985b6f1f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:04:54 +0000 Subject: [PATCH 18/38] refactor(auth): use @slack/web-api SDK types in auth.model.ts Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/63fd4758-62b9-437b-9cbb-67d64c07ea15 --- packages/backend/src/auth/auth.model.ts | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/backend/src/auth/auth.model.ts b/packages/backend/src/auth/auth.model.ts index 700c2c92..e9857999 100644 --- a/packages/backend/src/auth/auth.model.ts +++ b/packages/backend/src/auth/auth.model.ts @@ -1,19 +1,12 @@ -export interface SlackTokenResponse { - ok: boolean; - authed_user?: { - id: string; - access_token: string; - }; -} +import type { OauthV2AccessResponse, UsersIdentityResponse } from '@slack/web-api'; -export interface SlackIdentityResponse { - ok: boolean; - user?: { - id: string; - name: string; - }; +// Re-use the Slack SDK type for the OAuth token exchange response. +export type SlackTokenResponse = OauthV2AccessResponse; + +// Re-use the Slack SDK identity response type and extend it with `team.domain`, +// which the SDK's auto-generated types omit but the users.identity API does return. +export type SlackIdentityResponse = UsersIdentityResponse & { team?: { - domain: string; - id: string; + domain?: string; }; -} +}; From 35950cbffb524ee65ebe1054c71cf831994a7d1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:09:11 +0000 Subject: [PATCH 19/38] fix(frontend): exclude spec/test files from tsconfig.json to fix production build Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/c0d2f0df-bbac-4adc-9240-2adb52c15aa1 --- packages/frontend/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json index be42d05a..09cf311b 100644 --- a/packages/frontend/tsconfig.json +++ b/packages/frontend/tsconfig.json @@ -22,5 +22,6 @@ }, "types": ["vite/client"] }, - "include": ["src"] + "include": ["src"], + "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx", "src/**/*.test.ts", "src/**/*.test.tsx", "src/test"] } From 49d0b4cf79bd451e632dd403a276691dc986442f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:01:13 +0000 Subject: [PATCH 20/38] refactor: move consts/models to dedicated files, enforce 80% coverage, clean up App.tsx Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/f4301097-fe3b-4f58-9d43-e4fc90c16d7b --- packages/backend/src/auth/auth.const.ts | 6 +++ .../backend/src/auth/auth.controller.spec.ts | 15 +++++++ packages/backend/src/auth/auth.controller.ts | 35 ++++++++++------- packages/backend/src/auth/auth.model.ts | 12 ------ packages/backend/src/search/search.const.ts | 4 ++ .../backend/src/search/search.controller.ts | 3 +- packages/backend/src/search/search.model.ts | 6 +++ .../src/search/search.persistence.service.ts | 21 ++++------ .../src/shared/middleware/authMiddleware.ts | 3 +- .../src/shared/utils/session-token.const.ts | 2 + .../src/shared/utils/session-token.model.ts | 5 +++ .../backend/src/shared/utils/session-token.ts | 12 +++--- packages/frontend/src/App.spec.tsx | 8 ++-- packages/frontend/src/App.tsx | 39 ++++++------------- packages/frontend/src/app.const.ts | 1 + packages/frontend/src/app.model.ts | 9 +++++ packages/frontend/vite.config.ts | 2 + 17 files changed, 103 insertions(+), 80 deletions(-) create mode 100644 packages/backend/src/auth/auth.const.ts delete mode 100644 packages/backend/src/auth/auth.model.ts create mode 100644 packages/backend/src/search/search.const.ts create mode 100644 packages/backend/src/search/search.model.ts create mode 100644 packages/backend/src/shared/utils/session-token.const.ts create mode 100644 packages/backend/src/shared/utils/session-token.model.ts create mode 100644 packages/frontend/src/app.const.ts create mode 100644 packages/frontend/src/app.model.ts diff --git a/packages/backend/src/auth/auth.const.ts b/packages/backend/src/auth/auth.const.ts new file mode 100644 index 00000000..e41a6b43 --- /dev/null +++ b/packages/backend/src/auth/auth.const.ts @@ -0,0 +1,6 @@ +export const ALLOWED_TEAM_DOMAIN = 'dabros2016'; +export const SLACK_AUTH_URL = 'https://slack.com/oauth/v2/authorize'; +export const SLACK_TOKEN_URL = 'https://slack.com/api/oauth.v2.access'; +export const SLACK_IDENTITY_URL = 'https://slack.com/api/users.identity'; +export const OAUTH_STATE_COOKIE = 'oauth_state'; +export const OAUTH_STATE_MAX_AGE_MS = 300000; // 5 minutes in milliseconds diff --git a/packages/backend/src/auth/auth.controller.spec.ts b/packages/backend/src/auth/auth.controller.spec.ts index f513af70..88308a1d 100644 --- a/packages/backend/src/auth/auth.controller.spec.ts +++ b/packages/backend/src/auth/auth.controller.spec.ts @@ -52,9 +52,24 @@ describe('authController', () => { const res = await request(app).get('/slack'); expect(res.status).toBe(500); }); + + it('returns 500 when SLACK_REDIRECT_URI is not set', async () => { + delete process.env.SLACK_REDIRECT_URI; + const res = await request(app).get('/slack'); + expect(res.status).toBe(500); + }); }); describe('GET /slack/callback', () => { + it('returns 500 when SEARCH_FRONTEND_URL is not set', async () => { + delete process.env.SEARCH_FRONTEND_URL; + const res = await request(app) + .get('/slack/callback') + .set('Cookie', STATE_COOKIE) + .query({ code: 'valid-code', state: TEST_STATE }); + expect(res.status).toBe(500); + }); + it('redirects to frontend with token in hash on successful OAuth', async () => { (Axios.post as jest.Mock).mockResolvedValue({ data: { ok: true, authed_user: { id: 'U123', access_token: 'xoxp-token' } }, diff --git a/packages/backend/src/auth/auth.controller.ts b/packages/backend/src/auth/auth.controller.ts index 105fd4ee..ce7c3c8e 100644 --- a/packages/backend/src/auth/auth.controller.ts +++ b/packages/backend/src/auth/auth.controller.ts @@ -2,21 +2,25 @@ import crypto from 'crypto'; import type { Request, Router } from 'express'; import express from 'express'; import Axios from 'axios'; +import type { OauthV2AccessResponse, UsersIdentityResponse } from '@slack/web-api'; import { createSessionToken } from '../shared/utils/session-token'; import { logError } from '../shared/logger/error-logging'; import { logger } from '../shared/logger/logger'; -import type { SlackIdentityResponse, SlackTokenResponse } from './auth.model'; +import { + ALLOWED_TEAM_DOMAIN, + SLACK_AUTH_URL, + SLACK_TOKEN_URL, + SLACK_IDENTITY_URL, + OAUTH_STATE_COOKIE, + OAUTH_STATE_MAX_AGE_MS, +} from './auth.const'; + +type SlackTokenResponse = OauthV2AccessResponse; +type SlackIdentityResponse = UsersIdentityResponse & { team?: { domain?: string } }; export const authController: Router = express.Router(); const authLogger = logger.child({ module: 'AuthController' }); -const ALLOWED_TEAM_DOMAIN = 'dabros2016'; -const SLACK_AUTH_URL = 'https://slack.com/oauth/v2/authorize'; -const SLACK_TOKEN_URL = 'https://slack.com/api/oauth.v2.access'; -const SLACK_IDENTITY_URL = 'https://slack.com/api/users.identity'; -const OAUTH_STATE_COOKIE = 'oauth_state'; -const OAUTH_STATE_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes - function getCookieValue(req: Request, name: string): string | undefined { const cookieHeader = req.headers.cookie; if (!cookieHeader) return undefined; @@ -27,9 +31,9 @@ function getCookieValue(req: Request, name: string): string | undefined { authController.get('/slack', (_req, res) => { const clientId = process.env.SLACK_CLIENT_ID; - const redirectUri = process.env.SLACK_REDIRECT_URI ?? 'http://localhost:3000/auth/slack/callback'; + const redirectUri = process.env.SLACK_REDIRECT_URI; - if (!clientId) { + if (!clientId || !redirectUri) { res.status(500).send('Slack OAuth is not configured'); return; } @@ -53,7 +57,12 @@ authController.get('/slack', (_req, res) => { }); authController.get('/slack/callback', (req, res) => { - const frontendUrl = process.env.SEARCH_FRONTEND_URL ?? 'http://localhost:5173'; + const frontendUrl = process.env.SEARCH_FRONTEND_URL; + + if (!frontendUrl) { + res.status(500).send('Frontend URL is not configured'); + return; + } void (async () => { const { code, error, state: stateFromQuery } = req.query; @@ -73,9 +82,9 @@ authController.get('/slack/callback', (req, res) => { const clientId = process.env.SLACK_CLIENT_ID; const clientSecret = process.env.SLACK_CLIENT_SECRET; - const redirectUri = process.env.SLACK_REDIRECT_URI ?? 'http://localhost:3000/auth/slack/callback'; + const redirectUri = process.env.SLACK_REDIRECT_URI; - if (!clientId || !clientSecret) { + if (!clientId || !clientSecret || !redirectUri) { res.status(500).send('Slack OAuth is not configured'); return; } diff --git a/packages/backend/src/auth/auth.model.ts b/packages/backend/src/auth/auth.model.ts deleted file mode 100644 index e9857999..00000000 --- a/packages/backend/src/auth/auth.model.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { OauthV2AccessResponse, UsersIdentityResponse } from '@slack/web-api'; - -// Re-use the Slack SDK type for the OAuth token exchange response. -export type SlackTokenResponse = OauthV2AccessResponse; - -// Re-use the Slack SDK identity response type and extend it with `team.domain`, -// which the SDK's auto-generated types omit but the users.identity API does return. -export type SlackIdentityResponse = UsersIdentityResponse & { - team?: { - domain?: string; - }; -}; diff --git a/packages/backend/src/search/search.const.ts b/packages/backend/src/search/search.const.ts new file mode 100644 index 00000000..9f003b11 --- /dev/null +++ b/packages/backend/src/search/search.const.ts @@ -0,0 +1,4 @@ +export const MAX_LIMIT = 1000; +export const DEFAULT_LIMIT = 100; +export const MIN_LIMIT = 1; +export const PERSISTENCE_MAX_LIMIT = 500; diff --git a/packages/backend/src/search/search.controller.ts b/packages/backend/src/search/search.controller.ts index 84e2cafc..d8973417 100644 --- a/packages/backend/src/search/search.controller.ts +++ b/packages/backend/src/search/search.controller.ts @@ -3,14 +3,13 @@ import express from 'express'; import { SearchPersistenceService } from './search.persistence.service'; import { logError } from '../shared/logger/error-logging'; import { logger } from '../shared/logger/logger'; +import { MAX_LIMIT } from './search.const'; export const searchController: Router = express.Router(); const searchPersistenceService = new SearchPersistenceService(); const searchLogger = logger.child({ module: 'SearchController' }); -const MAX_LIMIT = 1000; - searchController.get('/messages', (req, res) => { const { userName, channel, content, limit } = req.query; diff --git a/packages/backend/src/search/search.model.ts b/packages/backend/src/search/search.model.ts new file mode 100644 index 00000000..29f6922b --- /dev/null +++ b/packages/backend/src/search/search.model.ts @@ -0,0 +1,6 @@ +export interface MessageSearchParams { + userName?: string; + channel?: string; + content?: string; + limit?: number; +} diff --git a/packages/backend/src/search/search.persistence.service.ts b/packages/backend/src/search/search.persistence.service.ts index e5ae3e91..2359775f 100644 --- a/packages/backend/src/search/search.persistence.service.ts +++ b/packages/backend/src/search/search.persistence.service.ts @@ -3,32 +3,25 @@ import { Message } from '../shared/db/models/Message'; import type { MessageWithName } from '../shared/models/message/message-with-name'; import { logError } from '../shared/logger/error-logging'; import { logger } from '../shared/logger/logger'; - -export interface MessageSearchParams { - userName?: string; - channel?: string; - content?: string; - limit?: number; -} +import type { MessageSearchParams } from './search.model'; +import { DEFAULT_LIMIT, MIN_LIMIT, PERSISTENCE_MAX_LIMIT } from './search.const'; export class SearchPersistenceService { private logger = logger.child({ module: 'SearchPersistenceService' }); - private static readonly DEFAULT_LIMIT = 100; - private static readonly MIN_LIMIT = 1; - private static readonly MAX_LIMIT = 500; - private static resolveLimit(limit: number | undefined): number { - if (limit === undefined || !Number.isFinite(limit) || limit < SearchPersistenceService.MIN_LIMIT) { - return SearchPersistenceService.DEFAULT_LIMIT; + if (limit === undefined || !Number.isFinite(limit) || limit < MIN_LIMIT) { + return DEFAULT_LIMIT; } - return Math.min(limit, SearchPersistenceService.MAX_LIMIT); + return Math.min(limit, PERSISTENCE_MAX_LIMIT); } async searchMessages(params: MessageSearchParams): Promise { const { userName, channel, content } = params; const effectiveLimit = SearchPersistenceService.resolveLimit(params.limit); + // Each condition is joined with AND in the WHERE clause; queryParams holds the + // positional `?` values in the same order as the conditions that use them. const conditions: string[] = ["message.message != ''"]; const queryParams: (string | number)[] = []; diff --git a/packages/backend/src/shared/middleware/authMiddleware.ts b/packages/backend/src/shared/middleware/authMiddleware.ts index c84bf93c..a4dcf8b3 100644 --- a/packages/backend/src/shared/middleware/authMiddleware.ts +++ b/packages/backend/src/shared/middleware/authMiddleware.ts @@ -1,5 +1,6 @@ import type { Request, Response, NextFunction } from 'express'; import { verifySessionToken } from '../utils/session-token'; +import { BEARER_PREFIX_LENGTH } from '../utils/session-token.const'; import { logger } from '../logger/logger'; const authLogger = logger.child({ module: 'AuthMiddleware' }); @@ -11,7 +12,7 @@ export const authMiddleware = (req: Request, res: Response, next: NextFunction): return; } - const token = authHeader.slice(7); + const token = authHeader.slice(BEARER_PREFIX_LENGTH); let session: ReturnType; try { session = verifySessionToken(token); diff --git a/packages/backend/src/shared/utils/session-token.const.ts b/packages/backend/src/shared/utils/session-token.const.ts new file mode 100644 index 00000000..3fcbf4a4 --- /dev/null +++ b/packages/backend/src/shared/utils/session-token.const.ts @@ -0,0 +1,2 @@ +export const TOKEN_TTL_MS = 86400000; // 24 hours in milliseconds +export const BEARER_PREFIX_LENGTH = 7; // Length of 'Bearer ' diff --git a/packages/backend/src/shared/utils/session-token.model.ts b/packages/backend/src/shared/utils/session-token.model.ts new file mode 100644 index 00000000..e85eecca --- /dev/null +++ b/packages/backend/src/shared/utils/session-token.model.ts @@ -0,0 +1,5 @@ +export interface SessionPayload { + userId: string; + teamDomain: string; + exp: number; +} diff --git a/packages/backend/src/shared/utils/session-token.ts b/packages/backend/src/shared/utils/session-token.ts index ca8de1c8..71250081 100644 --- a/packages/backend/src/shared/utils/session-token.ts +++ b/packages/backend/src/shared/utils/session-token.ts @@ -1,6 +1,8 @@ import crypto from 'crypto'; +import { TOKEN_TTL_MS } from './session-token.const'; +import type { SessionPayload } from './session-token.model'; -const TOKEN_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours +export type { SessionPayload }; function getSecret(): string { const secret = process.env.SEARCH_AUTH_SECRET; @@ -24,12 +26,6 @@ export function createSessionToken(userId: string, teamDomain: string): string { return `${payload}.${sig}`; } -export interface SessionPayload { - userId: string; - teamDomain: string; - exp: number; -} - function isSessionPayload(value: unknown): value is SessionPayload { if (!value || typeof value !== 'object') { return false; @@ -57,6 +53,8 @@ export function verifySessionToken(token: string): SessionPayload | null { } try { + // We control what was serialized into this token; the isSessionPayload guard + // validates the shape at runtime before we return it. const parsed: unknown = JSON.parse(Buffer.from(payload, 'base64url').toString()); if (!isSessionPayload(parsed)) return null; if (parsed.exp < Date.now()) return null; diff --git a/packages/frontend/src/App.spec.tsx b/packages/frontend/src/App.spec.tsx index 81d45b35..9655ac40 100644 --- a/packages/frontend/src/App.spec.tsx +++ b/packages/frontend/src/App.spec.tsx @@ -29,14 +29,14 @@ describe('App – token in URL hash', () => { render(); expect(screen.getByText(/message search/i)).toBeInTheDocument(); expect(screen.getByLabelText(/user name/i)).toBeInTheDocument(); - expect(localStorage.getItem('auth_token')).toBe('test-token-123'); + expect(localStorage.getItem('muzzle.lol-auth-token')).toBe('test-token-123'); expect(window.location.hash).toBe(''); }); }); describe('App – authenticated state', () => { beforeEach(() => { - localStorage.setItem('auth_token', 'stored-token'); + localStorage.setItem('muzzle.lol-auth-token', 'stored-token'); }); it('shows the search UI when a token is already stored', () => { @@ -49,7 +49,7 @@ describe('App – authenticated state', () => { it('clears the token and returns to LoginPage on Sign out click', () => { render(); fireEvent.click(screen.getByRole('button', { name: /sign out/i })); - expect(localStorage.getItem('auth_token')).toBeNull(); + expect(localStorage.getItem('muzzle.lol-auth-token')).toBeNull(); expect(screen.getByRole('link', { name: /sign in with slack/i })).toBeInTheDocument(); }); @@ -159,6 +159,6 @@ describe('App – authenticated state', () => { fireEvent.click(screen.getByRole('button', { name: /^search$/i })); await waitFor(() => expect(screen.getByRole('link', { name: /sign in with slack/i })).toBeInTheDocument()); - expect(localStorage.getItem('auth_token')).toBeNull(); + expect(localStorage.getItem('muzzle.lol-auth-token')).toBeNull(); }); }); diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index f4f12202..e4fb2974 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -8,28 +8,10 @@ import { Badge } from '@/components/ui/badge'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Separator } from '@/components/ui/separator'; import { LoginPage } from '@/components/LoginPage'; +import type { Message } from '@/app.model'; +import { AUTH_TOKEN_KEY } from '@/app.const'; -interface Message { - id: number; - message: string; - channel: string; - teamId: string; - createdAt: string; - name: string; - slackId: string; -} - -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000'; - -function formatDate(dateString: string): string { - return new Date(dateString).toLocaleString(); -} - -const AUTH_TOKEN_KEY = 'auth_token'; - -function getStoredToken(): string | null { - return localStorage.getItem(AUTH_TOKEN_KEY); -} +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string; export default function App() { const [isAuthenticated, setIsAuthenticated] = useState(false); @@ -53,7 +35,7 @@ export default function App() { localStorage.setItem(AUTH_TOKEN_KEY, tokenFromHash); window.history.replaceState({}, '', window.location.pathname); setIsAuthenticated(true); - } else if (getStoredToken()) { + } else if (localStorage.getItem(AUTH_TOKEN_KEY)) { setIsAuthenticated(true); } @@ -68,6 +50,8 @@ export default function App() { setIsAuthenticated(false); setMessages([]); setHasSearched(false); + setError(null); + setAuthError(undefined); }, []); const handleSearch = useCallback(async () => { @@ -80,13 +64,12 @@ export default function App() { if (content.trim()) params.set('content', content.trim()); try { - const token = getStoredToken() ?? ''; + const token = localStorage.getItem(AUTH_TOKEN_KEY) ?? ''; const response = await fetch(`${API_BASE_URL}/search/messages?${params.toString()}`, { headers: { Authorization: `Bearer ${token}` }, }); if (response.status === 401) { - localStorage.removeItem(AUTH_TOKEN_KEY); - setIsAuthenticated(false); + handleLogout(); return; } if (!response.ok) { @@ -100,7 +83,7 @@ export default function App() { } finally { setIsLoading(false); } - }, [userName, channel, content]); + }, [userName, channel, content, handleLogout]); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { @@ -231,7 +214,9 @@ export default function App() { {msg.message} - {formatDate(msg.createdAt)} + + {new Date(msg.createdAt).toLocaleString()} + ))} diff --git a/packages/frontend/src/app.const.ts b/packages/frontend/src/app.const.ts new file mode 100644 index 00000000..7e1baf44 --- /dev/null +++ b/packages/frontend/src/app.const.ts @@ -0,0 +1 @@ +export const AUTH_TOKEN_KEY = 'muzzle.lol-auth-token'; diff --git a/packages/frontend/src/app.model.ts b/packages/frontend/src/app.model.ts new file mode 100644 index 00000000..4e318021 --- /dev/null +++ b/packages/frontend/src/app.model.ts @@ -0,0 +1,9 @@ +export interface Message { + id: number; + message: string; + channel: string; + teamId: string; + createdAt: string; + name: string; + slackId: string; +} diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 9b15dad6..716933b9 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -15,6 +15,8 @@ export default defineConfig({ setupFiles: ['./src/test/setup.ts'], coverage: { provider: 'v8', + include: ['src/**/*.{ts,tsx}'], + exclude: ['src/**/*.spec.{ts,tsx}', 'src/**/*.test.{ts,tsx}', 'src/test/**', 'src/main.tsx', 'src/**/*.d.ts'], thresholds: { branches: 80, functions: 80, From da9cac8bf041243695c60e7b2ec0ca2a9ef35e20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:08:36 +0000 Subject: [PATCH 21/38] refactor: address review feedback - SDK types, ternary, secret guard, useAuth hook, search-as-you-type Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/62ef6747-64ec-4791-912c-57d56c1c397e Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> --- packages/backend/src/auth/auth.controller.ts | 10 ++-- .../src/search/search.persistence.service.ts | 7 +-- .../src/shared/utils/session-token.spec.ts | 5 +- .../backend/src/shared/utils/session-token.ts | 3 - packages/frontend/src/App.spec.tsx | 8 +-- packages/frontend/src/App.tsx | 59 +++++++------------ packages/frontend/src/hooks/useAuth.ts | 43 ++++++++++++++ 7 files changed, 76 insertions(+), 59 deletions(-) create mode 100644 packages/frontend/src/hooks/useAuth.ts diff --git a/packages/backend/src/auth/auth.controller.ts b/packages/backend/src/auth/auth.controller.ts index ce7c3c8e..d0a615df 100644 --- a/packages/backend/src/auth/auth.controller.ts +++ b/packages/backend/src/auth/auth.controller.ts @@ -15,9 +15,6 @@ import { OAUTH_STATE_MAX_AGE_MS, } from './auth.const'; -type SlackTokenResponse = OauthV2AccessResponse; -type SlackIdentityResponse = UsersIdentityResponse & { team?: { domain?: string } }; - export const authController: Router = express.Router(); const authLogger = logger.child({ module: 'AuthController' }); @@ -89,7 +86,7 @@ authController.get('/slack/callback', (req, res) => { return; } - const tokenResponse = await Axios.post( + const tokenResponse = await Axios.post( SLACK_TOKEN_URL, new URLSearchParams({ client_id: clientId, @@ -105,11 +102,12 @@ authController.get('/slack/callback', (req, res) => { return; } - const identityResponse = await Axios.get(SLACK_IDENTITY_URL, { + const identityResponse = await Axios.get(SLACK_IDENTITY_URL, { headers: { Authorization: `Bearer ${tokenResponse.data.authed_user.access_token}` }, }); - const teamDomain = identityResponse.data.team?.domain; + // The Slack users.identity API returns team.domain but the SDK type only declares team.id. + const teamDomain = (identityResponse.data.team as { id?: string; domain?: string } | undefined)?.domain; const userId = identityResponse.data.user?.id; if (!identityResponse.data.ok || teamDomain !== ALLOWED_TEAM_DOMAIN || !userId) { res.redirect(`${frontendUrl}?auth_error=unauthorized_workspace`); diff --git a/packages/backend/src/search/search.persistence.service.ts b/packages/backend/src/search/search.persistence.service.ts index 2359775f..be884509 100644 --- a/packages/backend/src/search/search.persistence.service.ts +++ b/packages/backend/src/search/search.persistence.service.ts @@ -10,10 +10,9 @@ export class SearchPersistenceService { private logger = logger.child({ module: 'SearchPersistenceService' }); private static resolveLimit(limit: number | undefined): number { - if (limit === undefined || !Number.isFinite(limit) || limit < MIN_LIMIT) { - return DEFAULT_LIMIT; - } - return Math.min(limit, PERSISTENCE_MAX_LIMIT); + return limit === undefined || !Number.isFinite(limit) || limit < MIN_LIMIT + ? DEFAULT_LIMIT + : Math.min(limit, PERSISTENCE_MAX_LIMIT); } async searchMessages(params: MessageSearchParams): Promise { diff --git a/packages/backend/src/shared/utils/session-token.spec.ts b/packages/backend/src/shared/utils/session-token.spec.ts index 4d849028..afec84e4 100644 --- a/packages/backend/src/shared/utils/session-token.spec.ts +++ b/packages/backend/src/shared/utils/session-token.spec.ts @@ -37,15 +37,12 @@ describe('session-token', () => { expect(decoded.exp).toBeGreaterThan(after); }); - it('throws when SEARCH_AUTH_SECRET is not set in a non-test environment', () => { - const origNodeEnv = process.env.NODE_ENV; + it('throws when SEARCH_AUTH_SECRET is not set', () => { delete process.env.SEARCH_AUTH_SECRET; - process.env.NODE_ENV = 'production'; try { expect(() => createSessionToken('U1', 'team')).toThrow(); } finally { process.env.SEARCH_AUTH_SECRET = 'test-secret'; - process.env.NODE_ENV = origNodeEnv ?? ''; } }); }); diff --git a/packages/backend/src/shared/utils/session-token.ts b/packages/backend/src/shared/utils/session-token.ts index 71250081..ae2eea5f 100644 --- a/packages/backend/src/shared/utils/session-token.ts +++ b/packages/backend/src/shared/utils/session-token.ts @@ -7,9 +7,6 @@ export type { SessionPayload }; function getSecret(): string { const secret = process.env.SEARCH_AUTH_SECRET; if (!secret) { - if (process.env.NODE_ENV === 'test') { - return 'insecure-test-secret'; - } throw new Error( 'SEARCH_AUTH_SECRET environment variable is not set. ' + 'Session tokens cannot be created or verified without a secret.', diff --git a/packages/frontend/src/App.spec.tsx b/packages/frontend/src/App.spec.tsx index 9655ac40..2712c78f 100644 --- a/packages/frontend/src/App.spec.tsx +++ b/packages/frontend/src/App.spec.tsx @@ -63,14 +63,14 @@ describe('App – authenticated state', () => { expect(screen.getByText(/content: hello/i)).toBeInTheDocument(); }); - it('triggers search on Enter keydown in an input', async () => { + it('triggers search after typing in an input', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: async () => [] }); render(); - fireEvent.keyDown(screen.getByLabelText(/user name/i), { key: 'Enter' }); - await waitFor(() => expect(screen.getByText(/no messages found/i)).toBeInTheDocument()); + fireEvent.change(screen.getByLabelText(/user name/i), { target: { value: 'alice' } }); + await waitFor(() => expect(screen.getByText(/no messages found/i)).toBeInTheDocument(), { timeout: 2000 }); }); - it('does not trigger search on a non-Enter keydown', async () => { + it('does not trigger search when no input values change', async () => { render(); fireEvent.keyDown(screen.getByLabelText(/user name/i), { key: 'a' }); expect(mockFetch).not.toHaveBeenCalled(); diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index e4fb2974..2b71f919 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback, useEffect, useRef } from 'react'; import { Search, Loader2, LogOut } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -10,12 +10,12 @@ import { Separator } from '@/components/ui/separator'; import { LoginPage } from '@/components/LoginPage'; import type { Message } from '@/app.model'; import { AUTH_TOKEN_KEY } from '@/app.const'; +import { useAuth } from '@/hooks/useAuth'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string; export default function App() { - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [authError, setAuthError] = useState(undefined); + const { isAuthenticated, authError, logout } = useAuth(); const [userName, setUserName] = useState(''); const [channel, setChannel] = useState(''); const [content, setContent] = useState(''); @@ -24,35 +24,13 @@ export default function App() { const [error, setError] = useState(null); const [hasSearched, setHasSearched] = useState(false); - useEffect(() => { - const urlParams = new URLSearchParams(window.location.search); - const errorFromUrl = urlParams.get('auth_error') ?? undefined; - - const hashParams = new URLSearchParams(window.location.hash.slice(1)); - const tokenFromHash = hashParams.get('token'); - - if (tokenFromHash) { - localStorage.setItem(AUTH_TOKEN_KEY, tokenFromHash); - window.history.replaceState({}, '', window.location.pathname); - setIsAuthenticated(true); - } else if (localStorage.getItem(AUTH_TOKEN_KEY)) { - setIsAuthenticated(true); - } - - if (errorFromUrl) { - setAuthError(errorFromUrl); - window.history.replaceState({}, '', window.location.pathname); - } - }, []); - const handleLogout = useCallback(() => { - localStorage.removeItem(AUTH_TOKEN_KEY); - setIsAuthenticated(false); - setMessages([]); - setHasSearched(false); - setError(null); - setAuthError(undefined); - }, []); + logout(() => { + setMessages([]); + setHasSearched(false); + setError(null); + }); + }, [logout]); const handleSearch = useCallback(async () => { setIsLoading(true); @@ -85,11 +63,19 @@ export default function App() { } }, [userName, channel, content, handleLogout]); - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - void handleSearch(); + // Debounced search-as-you-type: skip the initial render, then trigger a search + // 300ms after the user stops changing userName, channel, or content. + const isFirstRender = useRef(true); + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; } - }; + const timer = setTimeout(() => { + void handleSearch(); + }, 300); + return () => clearTimeout(timer); + }, [handleSearch]); const activeFilters = [ userName.trim() && { label: 'User', value: userName.trim() }, @@ -131,7 +117,6 @@ export default function App() { placeholder="e.g. john" value={userName} onChange={(e) => setUserName(e.target.value)} - onKeyDown={handleKeyDown} />
@@ -141,7 +126,6 @@ export default function App() { placeholder="e.g. general" value={channel} onChange={(e) => setChannel(e.target.value)} - onKeyDown={handleKeyDown} />
@@ -151,7 +135,6 @@ export default function App() { placeholder="e.g. hello world" value={content} onChange={(e) => setContent(e.target.value)} - onKeyDown={handleKeyDown} />
diff --git a/packages/frontend/src/hooks/useAuth.ts b/packages/frontend/src/hooks/useAuth.ts new file mode 100644 index 00000000..e57b505e --- /dev/null +++ b/packages/frontend/src/hooks/useAuth.ts @@ -0,0 +1,43 @@ +import { useState, useCallback, useEffect } from 'react'; +import { AUTH_TOKEN_KEY } from '@/app.const'; + +export interface UseAuthReturn { + isAuthenticated: boolean; + authError: string | undefined; + logout: (onLogout?: () => void) => void; +} + +export function useAuth(): UseAuthReturn { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [authError, setAuthError] = useState(undefined); + + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const errorFromUrl = urlParams.get('auth_error') ?? undefined; + + const hashParams = new URLSearchParams(window.location.hash.slice(1)); + const tokenFromHash = hashParams.get('token'); + + if (tokenFromHash) { + localStorage.setItem(AUTH_TOKEN_KEY, tokenFromHash); + window.history.replaceState({}, '', window.location.pathname); + setIsAuthenticated(true); + } else if (localStorage.getItem(AUTH_TOKEN_KEY)) { + setIsAuthenticated(true); + } + + if (errorFromUrl) { + setAuthError(errorFromUrl); + window.history.replaceState({}, '', window.location.pathname); + } + }, []); + + const logout = useCallback((onLogout?: () => void) => { + localStorage.removeItem(AUTH_TOKEN_KEY); + setIsAuthenticated(false); + setAuthError(undefined); + onLogout?.(); + }, []); + + return { isAuthenticated, authError, logout }; +} From fced44bb8c9e6b5e8f35e396c0b7cf5a0f014fb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:29:43 +0000 Subject: [PATCH 22/38] fix: resolve lint errors in auth.controller.ts and useAuth.ts Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/12f1913a-e7cd-42a1-a875-89fa51a62404 Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> --- packages/backend/src/auth/auth.controller.ts | 11 ++++-- packages/frontend/src/hooks/useAuth.ts | 40 +++++++++++--------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/packages/backend/src/auth/auth.controller.ts b/packages/backend/src/auth/auth.controller.ts index d0a615df..035df198 100644 --- a/packages/backend/src/auth/auth.controller.ts +++ b/packages/backend/src/auth/auth.controller.ts @@ -15,6 +15,12 @@ import { OAUTH_STATE_MAX_AGE_MS, } from './auth.const'; +// The Slack users.identity API returns team.domain but the SDK type only declares team.id. +// Use an extended type that reflects the actual API response shape. +type IdentityResponseWithTeamDomain = Omit & { + team?: { id?: string; domain?: string }; +}; + export const authController: Router = express.Router(); const authLogger = logger.child({ module: 'AuthController' }); @@ -102,12 +108,11 @@ authController.get('/slack/callback', (req, res) => { return; } - const identityResponse = await Axios.get(SLACK_IDENTITY_URL, { + const identityResponse = await Axios.get(SLACK_IDENTITY_URL, { headers: { Authorization: `Bearer ${tokenResponse.data.authed_user.access_token}` }, }); - // The Slack users.identity API returns team.domain but the SDK type only declares team.id. - const teamDomain = (identityResponse.data.team as { id?: string; domain?: string } | undefined)?.domain; + const teamDomain = identityResponse.data.team?.domain; const userId = identityResponse.data.user?.id; if (!identityResponse.data.ok || teamDomain !== ALLOWED_TEAM_DOMAIN || !userId) { res.redirect(`${frontendUrl}?auth_error=unauthorized_workspace`); diff --git a/packages/frontend/src/hooks/useAuth.ts b/packages/frontend/src/hooks/useAuth.ts index e57b505e..71db2787 100644 --- a/packages/frontend/src/hooks/useAuth.ts +++ b/packages/frontend/src/hooks/useAuth.ts @@ -7,27 +7,31 @@ export interface UseAuthReturn { logout: (onLogout?: () => void) => void; } -export function useAuth(): UseAuthReturn { - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [authError, setAuthError] = useState(undefined); - - useEffect(() => { - const urlParams = new URLSearchParams(window.location.search); - const errorFromUrl = urlParams.get('auth_error') ?? undefined; +// Lazy initializer: reads the token from the URL hash or localStorage once at mount. +// localStorage.setItem is called here (before the first render) when a hash token is found. +function readInitialAuthState(): boolean { + const hashParams = new URLSearchParams(window.location.hash.slice(1)); + const tokenFromHash = hashParams.get('token'); + if (tokenFromHash) { + localStorage.setItem(AUTH_TOKEN_KEY, tokenFromHash); + } + return !!tokenFromHash || !!localStorage.getItem(AUTH_TOKEN_KEY); +} - const hashParams = new URLSearchParams(window.location.hash.slice(1)); - const tokenFromHash = hashParams.get('token'); +// Lazy initializer: reads the auth_error query param once at mount. +function readInitialAuthError(): string | undefined { + const urlParams = new URLSearchParams(window.location.search); + return urlParams.get('auth_error') ?? undefined; +} - if (tokenFromHash) { - localStorage.setItem(AUTH_TOKEN_KEY, tokenFromHash); - window.history.replaceState({}, '', window.location.pathname); - setIsAuthenticated(true); - } else if (localStorage.getItem(AUTH_TOKEN_KEY)) { - setIsAuthenticated(true); - } +export function useAuth(): UseAuthReturn { + const [isAuthenticated, setIsAuthenticated] = useState(readInitialAuthState); + const [authError, setAuthError] = useState(readInitialAuthError); - if (errorFromUrl) { - setAuthError(errorFromUrl); + // Side effects only: clean up the URL after reading the token / error from it. + // No setState calls here — initial state is already set via the lazy initializers above. + useEffect(() => { + if (window.location.hash || window.location.search.includes('auth_error')) { window.history.replaceState({}, '', window.location.pathname); } }, []); From 5b1d31caf1cf47bd0b17e388c032d4cd78c5a3b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:07:11 +0000 Subject: [PATCH 23/38] refactor(frontend): centralize API_BASE_URL into config.ts with fail-fast check Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/7858fa0f-3726-45c4-91cb-741892f7e260 --- packages/frontend/.env.test | 1 + packages/frontend/src/App.tsx | 3 +-- packages/frontend/src/components/LoginPage.tsx | 3 +-- packages/frontend/src/config.ts | 7 +++++++ 4 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 packages/frontend/.env.test create mode 100644 packages/frontend/src/config.ts diff --git a/packages/frontend/.env.test b/packages/frontend/.env.test new file mode 100644 index 00000000..99cbcd54 --- /dev/null +++ b/packages/frontend/.env.test @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:3000 diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 2b71f919..dd6484fd 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -11,8 +11,7 @@ import { LoginPage } from '@/components/LoginPage'; import type { Message } from '@/app.model'; import { AUTH_TOKEN_KEY } from '@/app.const'; import { useAuth } from '@/hooks/useAuth'; - -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string; +import { API_BASE_URL } from '@/config'; export default function App() { const { isAuthenticated, authError, logout } = useAuth(); diff --git a/packages/frontend/src/components/LoginPage.tsx b/packages/frontend/src/components/LoginPage.tsx index 57bd3958..1f261761 100644 --- a/packages/frontend/src/components/LoginPage.tsx +++ b/packages/frontend/src/components/LoginPage.tsx @@ -1,7 +1,6 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; - -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000'; +import { API_BASE_URL } from '@/config'; const AUTH_ERROR_MESSAGES: Record = { access_denied: 'Access was denied. Please try again.', diff --git a/packages/frontend/src/config.ts b/packages/frontend/src/config.ts new file mode 100644 index 00000000..e6c9c467 --- /dev/null +++ b/packages/frontend/src/config.ts @@ -0,0 +1,7 @@ +const apiBaseUrl = import.meta.env.VITE_API_BASE_URL; + +if (!apiBaseUrl) { + throw new Error('VITE_API_BASE_URL must be set. Add it to your .env file (see .env.example).'); +} + +export const API_BASE_URL: string = apiBaseUrl; From 13eb8ca06bf89f5f21722a0e8522d84526eedeef Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Fri, 27 Mar 2026 09:11:50 -0400 Subject: [PATCH 24/38] Update packages/backend/src/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/backend/src/index.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 9bfd25b8..a85b488c 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -38,8 +38,15 @@ import { authMiddleware } from './shared/middleware/authMiddleware'; const app: Application = express(); const PORT = process.env.PORT || 3000; +const SEARCH_UI_ORIGIN = process.env.SEARCH_FRONTEND_URL; + +if (!SEARCH_UI_ORIGIN) { + logger.error('Environment variable SEARCH_FRONTEND_URL must be set to configure CORS for search/auth routes.'); + throw new Error('Missing required environment variable: SEARCH_FRONTEND_URL'); +} + const searchCors = cors({ - origin: process.env.SEARCH_FRONTEND_URL || process.env.SEARCH_UI_ORIGIN || 'http://localhost:5173', + origin: SEARCH_UI_ORIGIN, }); app.use( From 20a4396e3ab636ab22d6787a1b6cc3ad816a1011 Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Fri, 27 Mar 2026 12:32:59 -0400 Subject: [PATCH 25/38] Update .github/workflows/ci.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a92790fc..db07eb5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup - - run: npm run lint + - run: npm run lint:backend lint-frontend: name: Lint (Front End) From a45dfa4366752799af977372e48d64a405970993 Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Fri, 27 Mar 2026 12:33:23 -0400 Subject: [PATCH 26/38] Update packages/backend/src/auth/auth.controller.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/backend/src/auth/auth.controller.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/auth/auth.controller.ts b/packages/backend/src/auth/auth.controller.ts index 035df198..b8dbb730 100644 --- a/packages/backend/src/auth/auth.controller.ts +++ b/packages/backend/src/auth/auth.controller.ts @@ -113,13 +113,14 @@ authController.get('/slack/callback', (req, res) => { }); const teamDomain = identityResponse.data.team?.domain; + const teamId = identityResponse.data.team?.id; const userId = identityResponse.data.user?.id; if (!identityResponse.data.ok || teamDomain !== ALLOWED_TEAM_DOMAIN || !userId) { res.redirect(`${frontendUrl}?auth_error=unauthorized_workspace`); return; } - const sessionToken = createSessionToken(userId, teamDomain); + const sessionToken = createSessionToken(userId, teamDomain, teamId); res.redirect(`${frontendUrl}#token=${sessionToken}`); })().catch((e: unknown) => { logError(authLogger, 'Slack OAuth callback failed', e, {}); From ffa07af5beb98b580c4079d5b926b206c848fe7d Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Fri, 27 Mar 2026 12:33:39 -0400 Subject: [PATCH 27/38] Update packages/backend/src/shared/utils/session-token.model.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/backend/src/shared/utils/session-token.model.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/src/shared/utils/session-token.model.ts b/packages/backend/src/shared/utils/session-token.model.ts index e85eecca..d225e55c 100644 --- a/packages/backend/src/shared/utils/session-token.model.ts +++ b/packages/backend/src/shared/utils/session-token.model.ts @@ -1,5 +1,6 @@ export interface SessionPayload { userId: string; teamDomain: string; + teamId?: string; exp: number; } From 4e7e8b24f695201626392b9807ffe8a47297482c Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Fri, 27 Mar 2026 16:01:06 -0400 Subject: [PATCH 28/38] Various fixes github wouldnt do on its own --- packages/backend/src/shared/utils/session-token.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/shared/utils/session-token.ts b/packages/backend/src/shared/utils/session-token.ts index ae2eea5f..f6395b50 100644 --- a/packages/backend/src/shared/utils/session-token.ts +++ b/packages/backend/src/shared/utils/session-token.ts @@ -15,10 +15,15 @@ function getSecret(): string { return secret; } -export function createSessionToken(userId: string, teamDomain: string): string { - const payload = Buffer.from(JSON.stringify({ userId, teamDomain, exp: Date.now() + TOKEN_TTL_MS })).toString( - 'base64url', - ); +export function createSessionToken(userId: string, teamDomain: string, teamId?: string): string { + const payloadData: SessionPayload = { + userId, + teamDomain, + exp: Date.now() + TOKEN_TTL_MS, + ...(teamId ? { teamId } : {}), + }; + + const payload = Buffer.from(JSON.stringify(payloadData)).toString('base64url'); const sig = crypto.createHmac('sha256', getSecret()).update(payload).digest('base64url'); return `${payload}.${sig}`; } @@ -30,6 +35,7 @@ function isSessionPayload(value: unknown): value is SessionPayload { return ( typeof Reflect.get(value, 'userId') === 'string' && typeof Reflect.get(value, 'teamDomain') === 'string' && + (Reflect.get(value, 'teamId') === undefined || typeof Reflect.get(value, 'teamId') === 'string') && typeof Reflect.get(value, 'exp') === 'number' ); } From 205bc15180b6f7f604fcd7da5de9b8d11a988ded Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Fri, 27 Mar 2026 16:14:42 -0400 Subject: [PATCH 29/38] Fixed bad AS --- .../src/search/search.controller.spec.ts | 30 ++++++++++++++ .../backend/src/search/search.controller.ts | 10 ++++- packages/backend/src/search/search.model.ts | 1 + .../search/search.persistence.service.spec.ts | 39 ++++++++++++------- .../src/search/search.persistence.service.ts | 7 ++-- .../shared/middleware/authMiddleware.spec.ts | 15 ++++++- .../src/shared/middleware/authMiddleware.ts | 13 ++++++- .../models/express/RequestWithAuthSession.ts | 6 +++ 8 files changed, 99 insertions(+), 22 deletions(-) create mode 100644 packages/backend/src/shared/models/express/RequestWithAuthSession.ts diff --git a/packages/backend/src/search/search.controller.spec.ts b/packages/backend/src/search/search.controller.spec.ts index fd55232b..454370ff 100644 --- a/packages/backend/src/search/search.controller.spec.ts +++ b/packages/backend/src/search/search.controller.spec.ts @@ -14,6 +14,15 @@ import { searchController } from './search.controller'; describe('searchController', () => { const app = express(); app.use(express.json()); + app.use((req, _res, next) => { + (req as { authSession?: { userId: string; teamDomain: string; teamId: string; exp: number } }).authSession = { + userId: 'U1', + teamDomain: 'dabros2016', + teamId: 'T1', + exp: Date.now() + 60000, + }; + next(); + }); app.use('/', searchController); beforeEach(() => jest.clearAllMocks()); @@ -29,6 +38,7 @@ describe('searchController', () => { expect(res.status).toBe(200); expect(res.body).toEqual(messages); expect(searchMessagesMock).toHaveBeenCalledWith({ + teamId: 'T1', userName: 'alice', channel: 'general', content: 'hello', @@ -42,6 +52,7 @@ describe('searchController', () => { await request(app).get('/messages').expect(200); expect(searchMessagesMock).toHaveBeenCalledWith({ + teamId: 'T1', userName: undefined, channel: undefined, content: undefined, @@ -80,4 +91,23 @@ describe('searchController', () => { expect(res.status).toBe(500); }); + + it('returns 401 when auth session is missing teamId', async () => { + const appWithoutTeam = express(); + appWithoutTeam.use(express.json()); + appWithoutTeam.use((req, _res, next) => { + (req as { authSession?: { userId: string; teamDomain: string; exp: number } }).authSession = { + userId: 'U1', + teamDomain: 'dabros2016', + exp: Date.now() + 60000, + }; + next(); + }); + appWithoutTeam.use('/', searchController); + + const res = await request(appWithoutTeam).get('/messages'); + + expect(res.status).toBe(401); + expect(searchMessagesMock).not.toHaveBeenCalled(); + }); }); diff --git a/packages/backend/src/search/search.controller.ts b/packages/backend/src/search/search.controller.ts index d8973417..4860719e 100644 --- a/packages/backend/src/search/search.controller.ts +++ b/packages/backend/src/search/search.controller.ts @@ -4,13 +4,20 @@ import { SearchPersistenceService } from './search.persistence.service'; import { logError } from '../shared/logger/error-logging'; import { logger } from '../shared/logger/logger'; import { MAX_LIMIT } from './search.const'; +import type { RequestWithAuthSession } from '../shared/models/express/RequestWithAuthSession'; export const searchController: Router = express.Router(); const searchPersistenceService = new SearchPersistenceService(); const searchLogger = logger.child({ module: 'SearchController' }); -searchController.get('/messages', (req, res) => { +searchController.get('/messages', (req: RequestWithAuthSession, res) => { + const teamId = req.authSession?.teamId; + if (!teamId) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + const { userName, channel, content, limit } = req.query; let parsedLimit: number | undefined; @@ -23,6 +30,7 @@ searchController.get('/messages', (req, res) => { searchPersistenceService .searchMessages({ + teamId, userName: typeof userName === 'string' ? userName : undefined, channel: typeof channel === 'string' ? channel : undefined, content: typeof content === 'string' ? content : undefined, diff --git a/packages/backend/src/search/search.model.ts b/packages/backend/src/search/search.model.ts index 29f6922b..9aa28158 100644 --- a/packages/backend/src/search/search.model.ts +++ b/packages/backend/src/search/search.model.ts @@ -1,4 +1,5 @@ export interface MessageSearchParams { + teamId: string; userName?: string; channel?: string; content?: string; diff --git a/packages/backend/src/search/search.persistence.service.spec.ts b/packages/backend/src/search/search.persistence.service.spec.ts index 7da7c486..7576cdfe 100644 --- a/packages/backend/src/search/search.persistence.service.spec.ts +++ b/packages/backend/src/search/search.persistence.service.spec.ts @@ -22,52 +22,61 @@ describe('SearchPersistenceService', () => { it('searches with no filters (default conditions only)', async () => { query.mockResolvedValue([{ id: 1, message: 'hello', name: 'alice' }]); - const result = await service.searchMessages({}); + const result = await service.searchMessages({ teamId: 'T1' }); - expect(query).toHaveBeenCalledWith(expect.stringContaining("message.message != ''"), [100]); + const [sql, params] = (query as jest.Mock).mock.calls[0] as [string, unknown[]]; + expect(sql).toContain("message.message != ''"); + expect(sql).toContain('message.teamId = ?'); + expect(sql).toContain('slack_user.teamId = ?'); + expect(params).toEqual(['T1', 'T1', 100]); expect(result).toEqual([{ id: 1, message: 'hello', name: 'alice' }]); }); it('applies userName LIKE filter when userName is provided', async () => { query.mockResolvedValue([]); - await service.searchMessages({ userName: 'alice' }); + await service.searchMessages({ teamId: 'T1', userName: 'alice' }); - expect(query).toHaveBeenCalledWith(expect.stringContaining('slack_user.name LIKE ?'), ['%alice%', 100]); + expect(query).toHaveBeenCalledWith(expect.stringContaining('slack_user.name LIKE ?'), ['T1', 'T1', '%alice%', 100]); }); it('applies channel LIKE filter when channel is provided', async () => { query.mockResolvedValue([]); - await service.searchMessages({ channel: 'general' }); + await service.searchMessages({ teamId: 'T1', channel: 'general' }); - expect(query).toHaveBeenCalledWith(expect.stringContaining('message.channel LIKE ?'), ['%general%', 100]); + expect(query).toHaveBeenCalledWith(expect.stringContaining('message.channel LIKE ?'), [ + 'T1', + 'T1', + '%general%', + 100, + ]); }); it('applies content LIKE filter when content is provided', async () => { query.mockResolvedValue([]); - await service.searchMessages({ content: 'hello' }); + await service.searchMessages({ teamId: 'T1', content: 'hello' }); - expect(query).toHaveBeenCalledWith(expect.stringContaining('message.message LIKE ?'), ['%hello%', 100]); + expect(query).toHaveBeenCalledWith(expect.stringContaining('message.message LIKE ?'), ['T1', 'T1', '%hello%', 100]); }); it('applies all three filters combined', async () => { query.mockResolvedValue([]); - await service.searchMessages({ userName: 'alice', channel: 'general', content: 'hello' }); + await service.searchMessages({ teamId: 'T1', userName: 'alice', channel: 'general', content: 'hello' }); const [sql, params] = (query as jest.Mock).mock.calls[0] as [string, unknown[]]; expect(sql).toContain('slack_user.name LIKE ?'); expect(sql).toContain('message.channel LIKE ?'); expect(sql).toContain('message.message LIKE ?'); - expect(params).toEqual(['%alice%', '%general%', '%hello%', 100]); + expect(params).toEqual(['T1', 'T1', '%alice%', '%general%', '%hello%', 100]); }); it('uses the provided limit instead of the default', async () => { query.mockResolvedValue([]); - await service.searchMessages({ limit: 25 }); + await service.searchMessages({ teamId: 'T1', limit: 25 }); const [, params] = (query as jest.Mock).mock.calls[0] as [string, unknown[]]; expect(params[params.length - 1]).toBe(25); @@ -76,7 +85,7 @@ describe('SearchPersistenceService', () => { it('clamps limit to MAX_LIMIT (500) when provided value exceeds it', async () => { query.mockResolvedValue([]); - await service.searchMessages({ limit: 9999 }); + await service.searchMessages({ teamId: 'T1', limit: 9999 }); const [, params] = (query as jest.Mock).mock.calls[0] as [string, unknown[]]; expect(params[params.length - 1]).toBe(500); @@ -85,7 +94,7 @@ describe('SearchPersistenceService', () => { it('uses default limit when provided value is below MIN_LIMIT', async () => { query.mockResolvedValue([]); - await service.searchMessages({ limit: 0 }); + await service.searchMessages({ teamId: 'T1', limit: 0 }); const [, params] = (query as jest.Mock).mock.calls[0] as [string, unknown[]]; expect(params[params.length - 1]).toBe(100); @@ -94,7 +103,7 @@ describe('SearchPersistenceService', () => { it('uses default limit when provided value is NaN', async () => { query.mockResolvedValue([]); - await service.searchMessages({ limit: NaN }); + await service.searchMessages({ teamId: 'T1', limit: NaN }); const [, params] = (query as jest.Mock).mock.calls[0] as [string, unknown[]]; expect(params[params.length - 1]).toBe(100); @@ -104,6 +113,6 @@ describe('SearchPersistenceService', () => { const error = new Error('DB error'); query.mockRejectedValue(error); - await expect(service.searchMessages({})).rejects.toThrow('DB error'); + await expect(service.searchMessages({ teamId: 'T1' })).rejects.toThrow('DB error'); }); }); diff --git a/packages/backend/src/search/search.persistence.service.ts b/packages/backend/src/search/search.persistence.service.ts index be884509..38ac5259 100644 --- a/packages/backend/src/search/search.persistence.service.ts +++ b/packages/backend/src/search/search.persistence.service.ts @@ -16,13 +16,13 @@ export class SearchPersistenceService { } async searchMessages(params: MessageSearchParams): Promise { - const { userName, channel, content } = params; + const { userName, channel, content, teamId } = params; const effectiveLimit = SearchPersistenceService.resolveLimit(params.limit); // Each condition is joined with AND in the WHERE clause; queryParams holds the // positional `?` values in the same order as the conditions that use them. - const conditions: string[] = ["message.message != ''"]; - const queryParams: (string | number)[] = []; + const conditions: string[] = ["message.message != ''", 'message.teamId = ?', 'slack_user.teamId = ?']; + const queryParams: (string | number)[] = [teamId, teamId]; if (userName) { conditions.push('slack_user.name LIKE ?'); @@ -56,6 +56,7 @@ export class SearchPersistenceService { .query(query, queryParams) .catch((e: unknown) => { logError(this.logger, 'Failed to search messages', e, { + teamId: params.teamId, userName: params.userName, channel: params.channel, content: params.content, diff --git a/packages/backend/src/shared/middleware/authMiddleware.spec.ts b/packages/backend/src/shared/middleware/authMiddleware.spec.ts index 3edda8ad..aa139a8e 100644 --- a/packages/backend/src/shared/middleware/authMiddleware.spec.ts +++ b/packages/backend/src/shared/middleware/authMiddleware.spec.ts @@ -28,7 +28,7 @@ describe('authMiddleware', () => { }); it('calls next for a valid Bearer token', () => { - const token = createSessionToken('U1', 'dabros2016'); + const token = createSessionToken('U1', 'dabros2016', 'T1'); const req = makeReq(`Bearer ${token}`); const res = makeRes(); const next = jest.fn() as unknown as NextFunction; @@ -37,6 +37,19 @@ describe('authMiddleware', () => { expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); + expect((req as Request & { authSession?: { teamId?: string } }).authSession?.teamId).toBe('T1'); + }); + + it('returns 401 when verified token payload has no teamId', () => { + const token = createSessionToken('U1', 'dabros2016'); + const req = makeReq(`Bearer ${token}`); + const res = makeRes(); + const next = jest.fn() as unknown as NextFunction; + + authMiddleware(req as Request, res as Response, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(401); }); it('returns 401 when Authorization header is missing', () => { diff --git a/packages/backend/src/shared/middleware/authMiddleware.ts b/packages/backend/src/shared/middleware/authMiddleware.ts index a4dcf8b3..0c9d3e5b 100644 --- a/packages/backend/src/shared/middleware/authMiddleware.ts +++ b/packages/backend/src/shared/middleware/authMiddleware.ts @@ -1,11 +1,12 @@ -import type { Request, Response, NextFunction } from 'express'; +import type { Response, NextFunction } from 'express'; import { verifySessionToken } from '../utils/session-token'; import { BEARER_PREFIX_LENGTH } from '../utils/session-token.const'; import { logger } from '../logger/logger'; +import type { RequestWithAuthSession } from '../models/express/RequestWithAuthSession'; const authLogger = logger.child({ module: 'AuthMiddleware' }); -export const authMiddleware = (req: Request, res: Response, next: NextFunction): void => { +export const authMiddleware = (req: RequestWithAuthSession, res: Response, next: NextFunction): void => { const authHeader = req.headers.authorization; if (!authHeader?.startsWith('Bearer ')) { res.status(401).json({ error: 'Unauthorized' }); @@ -28,5 +29,13 @@ export const authMiddleware = (req: Request, res: Response, next: NextFunction): return; } + if (!session.teamId) { + authLogger.warn('Session token missing teamId; rejecting request'); + res.status(401).json({ error: 'Unauthorized' }); + return; + } + + req.authSession = session; + next(); }; diff --git a/packages/backend/src/shared/models/express/RequestWithAuthSession.ts b/packages/backend/src/shared/models/express/RequestWithAuthSession.ts new file mode 100644 index 00000000..86ef4f6a --- /dev/null +++ b/packages/backend/src/shared/models/express/RequestWithAuthSession.ts @@ -0,0 +1,6 @@ +import type { Request } from 'express'; +import type { SessionPayload } from '../../utils/session-token.model'; + +export interface RequestWithAuthSession extends Request { + authSession?: SessionPayload; +} From 6cfd03f9b3523f0f2cb26c5b436f8ac63b20db46 Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Fri, 27 Mar 2026 16:54:19 -0400 Subject: [PATCH 30/38] Update packages/backend/src/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/backend/src/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 83f09c69..56b8b522 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -43,12 +43,13 @@ const PORT = process.env.PORT || 3000; const SEARCH_UI_ORIGIN = process.env.SEARCH_FRONTEND_URL; if (!SEARCH_UI_ORIGIN) { - logger.error('Environment variable SEARCH_FRONTEND_URL must be set to configure CORS for search/auth routes.'); - throw new Error('Missing required environment variable: SEARCH_FRONTEND_URL'); + logger.warn( + 'Environment variable SEARCH_FRONTEND_URL is not set; defaulting to disabling CORS for search/auth routes.', + ); } const searchCors = cors({ - origin: SEARCH_UI_ORIGIN, + origin: SEARCH_UI_ORIGIN || false, }); app.use( From 53d12bfc6efe0897dd0c65e200bc6fba2cb3a080 Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Fri, 27 Mar 2026 16:55:05 -0400 Subject: [PATCH 31/38] Update packages/frontend/src/App.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/frontend/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index dd6484fd..f6c7ead7 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -176,7 +176,7 @@ export default function App() { {messages.length > 0 && ( <> - + From bb393d72f3b08ef44a4e83fbb044fd28d00e93d3 Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Fri, 27 Mar 2026 16:58:11 -0400 Subject: [PATCH 32/38] Fixed code review feedback --- packages/backend/src/auth/auth.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/auth/auth.controller.ts b/packages/backend/src/auth/auth.controller.ts index b8dbb730..5228b19f 100644 --- a/packages/backend/src/auth/auth.controller.ts +++ b/packages/backend/src/auth/auth.controller.ts @@ -115,7 +115,7 @@ authController.get('/slack/callback', (req, res) => { const teamDomain = identityResponse.data.team?.domain; const teamId = identityResponse.data.team?.id; const userId = identityResponse.data.user?.id; - if (!identityResponse.data.ok || teamDomain !== ALLOWED_TEAM_DOMAIN || !userId) { + if (!identityResponse.data.ok || teamDomain !== ALLOWED_TEAM_DOMAIN || !userId || !teamId) { res.redirect(`${frontendUrl}?auth_error=unauthorized_workspace`); return; } From 46b0b9e36e92eca627dd2ac270149713fe815310 Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Fri, 27 Mar 2026 16:58:43 -0400 Subject: [PATCH 33/38] Fixed MAX_LIMIT --- packages/backend/src/search/search.const.ts | 1 - packages/backend/src/search/search.persistence.service.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/search/search.const.ts b/packages/backend/src/search/search.const.ts index 9f003b11..c62238da 100644 --- a/packages/backend/src/search/search.const.ts +++ b/packages/backend/src/search/search.const.ts @@ -1,4 +1,3 @@ export const MAX_LIMIT = 1000; export const DEFAULT_LIMIT = 100; export const MIN_LIMIT = 1; -export const PERSISTENCE_MAX_LIMIT = 500; diff --git a/packages/backend/src/search/search.persistence.service.ts b/packages/backend/src/search/search.persistence.service.ts index 38ac5259..1b999ec3 100644 --- a/packages/backend/src/search/search.persistence.service.ts +++ b/packages/backend/src/search/search.persistence.service.ts @@ -4,7 +4,7 @@ import type { MessageWithName } from '../shared/models/message/message-with-name import { logError } from '../shared/logger/error-logging'; import { logger } from '../shared/logger/logger'; import type { MessageSearchParams } from './search.model'; -import { DEFAULT_LIMIT, MIN_LIMIT, PERSISTENCE_MAX_LIMIT } from './search.const'; +import { DEFAULT_LIMIT, MIN_LIMIT, MAX_LIMIT } from './search.const'; export class SearchPersistenceService { private logger = logger.child({ module: 'SearchPersistenceService' }); @@ -12,7 +12,7 @@ export class SearchPersistenceService { private static resolveLimit(limit: number | undefined): number { return limit === undefined || !Number.isFinite(limit) || limit < MIN_LIMIT ? DEFAULT_LIMIT - : Math.min(limit, PERSISTENCE_MAX_LIMIT); + : Math.min(limit, MAX_LIMIT); } async searchMessages(params: MessageSearchParams): Promise { From 900b7dc359256ebff771f35e261c442407a5699c Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Fri, 27 Mar 2026 16:59:53 -0400 Subject: [PATCH 34/38] Fixed test --- .../backend/src/search/search.persistence.service.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/search/search.persistence.service.spec.ts b/packages/backend/src/search/search.persistence.service.spec.ts index 7576cdfe..0d817617 100644 --- a/packages/backend/src/search/search.persistence.service.spec.ts +++ b/packages/backend/src/search/search.persistence.service.spec.ts @@ -82,13 +82,13 @@ describe('SearchPersistenceService', () => { expect(params[params.length - 1]).toBe(25); }); - it('clamps limit to MAX_LIMIT (500) when provided value exceeds it', async () => { + it('clamps limit to MAX_LIMIT (1000) when provided value exceeds it', async () => { query.mockResolvedValue([]); await service.searchMessages({ teamId: 'T1', limit: 9999 }); const [, params] = (query as jest.Mock).mock.calls[0] as [string, unknown[]]; - expect(params[params.length - 1]).toBe(500); + expect(params[params.length - 1]).toBe(1000); }); it('uses default limit when provided value is below MIN_LIMIT', async () => { From 72ef9dcce055257fbd1fcc7944ff83138cfb2eff Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Fri, 27 Mar 2026 17:07:02 -0400 Subject: [PATCH 35/38] Updated README --- README.md | 306 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 231 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 3bd86c2e..f4fb903a 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,250 @@ # Mocker -## A Slack app to annoy your friends. +A Slack app that lets you mock your friends. Features real-time reactions, reputation tracking, game modes (Muzzle, Backfire, Counter, etc.), AI-powered summaries, and a web-based search interface for message history with team-scoped access control. -## Project Structure +## Architecture -This project is organized as a monorepo using npm workspaces: +This project is organized as a **npm monorepo** with the following structure: ``` mocker/ ├── packages/ -│ ├── backend/ # @mocker/backend - Express API server with Slack integration -│ ├── frontend/ # @mocker/frontend - Frontend application -│ └── jobs/ # Scheduled jobs -│ ├── fun-fact-job/ -│ ├── health-job/ -│ └── pricing-job/ -├── package.json # Root package with workspace configuration -└── tsconfig.base.json +│ ├── backend/ # @mocker/backend - Express API server +│ │ # - Slack bot integration +│ │ # - REST APIs for Slack commands and events +│ │ # - Search endpoint (team-scoped, requires OAuth token) +│ │ # - Slack OAuth flow (/auth/slack, /auth/slack/callback) +│ │ +│ ├── frontend/ # @mocker/frontend - React + Vite +│ │ # - Message search UI +│ │ # - OAuth login flow +│ │ # - Requires session token in URL fragment +│ │ +│ └── jobs/ # Standalone bash scheduled jobs +│ ├── fun-fact-job/ # Daily fun facts via Slack API +│ ├── health-job/ # Health check endpoint +│ └── pricing-job/ # Price update job +│ +├── package.json # Root workspace config +├── tsconfig.base.json # Shared TypeScript config +└── eslint.config.js # Root ESLint config (flat config) ``` ## Getting Started -### Setting Up Your Slack Environment - -1. Set up a new slack workspace for development purposes. (https://slack.com/get-started#/create) -2. Go to: https://api.slack.com/apps and click Create New App -3. Choose your newly created workspace as your Development Workspace and click Create App. -4. Configure Ngrok for your newly created bot: https://api.slack.com/tutorials/tunneling-with-ngrok -5. Add your bot oauth token as MUZZLE_BOT_TOKEN and your bot user token as MUZZLE_BOT_USER_TOKEN to your environment variables. Alternatively, you can pass these in as command line arguments. -6. Your app should have the following features per the Slack management web app: - -- Slash Commands - - /mock - Request URL: `/mock` - - /define - Request URL: `/define` - - /muzzle - Request URL: `/muzzle` - - /muzzlestats - Request URL: `/muzzle/stats` - - /confess - Request URL: `/confess` - - /list - Request URL: `/list/add` - - /listreport - Request URL: `/list/retrieve` - - /listremove - Request URL: `/list/remove` - - /counter - Request URL: `/counter` - - /repstats - Request URL: `/rep/get` - - /walkie - Request URL: `/walkie` - -Each of the slash commands should have `Escape Channels, users and links sent to your app` checked. - -- Event Subscriptions - - Request URL: `/muzzle/handle` - - Subscribe to Workspace Events: - - messages.channels - - reaction_added - - reaction_removed - - team_join - -- Permissions - - admin - - channels:history - - chat:write:bot - - chat:write:user - - commands - - files:write:user - - groups:history - - reactions:read - - users.profile:read - - users:read - -### Setting Up Your MYSQL Instance - -1. Be sure to have mysql installed and configured. -2. Create a database called `mockerdbdev`. -3. `mysql -u -p < DB_SEED.sql` -4. You should now have a fully seeded database. - -### Running Locally - -1. `npm install` (from the root directory - this installs dependencies for all workspaces) -2. Add the following environment variables for typeORM: +### Prerequisites +- **Node.js** 20+ (for backend and frontend development) +- **MySQL** 5.7+ (for storing messages, users, game state) +- **Slack workspace** (for bot integration) +- **Ngrok** (optional, for local tunneling during development) + +### 1. Set Up Slack App + +1. Create a Slack workspace for development: https://slack.com/get-started#/create +2. Go to https://api.slack.com/apps and create a new app in your workspace. +3. Configure the app with the following settings: + +#### Slash Commands + +Add these slash commands with their request URLs: + +- `/mock` → `/mock` +- `/define` → `/define` +- `/muzzle` → `/muzzle` +- `/muzzlestats` → `/muzzle/stats` +- `/confess` → `/confess` +- `/list` → `/list/add` +- `/listreport` → `/list/retrieve` +- `/listremove` → `/list/remove` +- `/counter` → `/counter` +- `/repstats` → `/rep/get` +- `/walkie` → `/walkie` + +**Important:** Check `Escape Channels, users and links sent to your app` for all commands. + +#### Event Subscriptions + +- **Request URL:** `/muzzle/handle` +- **Subscribe to Workspace Events:** + - `messages.channels` + - `reaction_added` + - `reaction_removed` + - `team_join` + +#### OAuth & Permissions + +- **Redirect URLs (for search/auth UI):** `http://localhost:3001` (dev), or your deployed frontend URL +- **Scopes:** + - `admin` + - `channels:history` + - `chat:write:bot` + - `chat:write:user` + - `commands` + - `files:write:user` + - `groups:history` + - `reactions:read` + - `users.profile:read` + - `users:read` + - `users.identity` (for OAuth login flow) + +Copy your **Bot Token** and **User OAuth Token** from the app credentials page. + +### 2. Set Up MySQL Database + +```bash +# Ensure MySQL is running and create the database +mysql -u -p -e "CREATE DATABASE mockerdbdev;" + +# Seed the database (if DB_SEED.sql exists in repo root) +mysql -u -p mockerdbdev < DB_SEED.sql +``` + +### 3. Environment Variables + +Create `.env` files in `packages/backend` and `packages/frontend` (or set them globally). + +#### Backend (`packages/backend/.env`) + +```bash +# Slack Bot Credentials +MUZZLE_BOT_TOKEN=xoxb-your-bot-token +MUZZLE_BOT_USER_TOKEN=xoxp-your-user-token +MUZZLE_BOT_SIGNING_SECRET=your-signing-secret + +# Slack OAuth (for search/auth UI login) +SLACK_CLIENT_ID=your-client-id +SLACK_CLIENT_SECRET=your-client-secret +SLACK_REDIRECT_URI=http://localhost:3000/auth/slack/callback + +# Search & Auth +ALLOWED_TEAM_DOMAIN=your-workspace-domain +SEARCH_FRONTEND_URL=http://localhost:3001 +SEARCH_AUTH_SECRET=generate-a-random-secret-key + +# MySQL / TypeORM +TYPEORM_CONNECTION=mysql +TYPEORM_HOST=localhost +TYPEORM_PORT=3306 +TYPEORM_USERNAME=root +TYPEORM_PASSWORD=your-password +TYPEORM_DATABASE=mockerdbdev +TYPEORM_ENTITIES=/absolute/path/to/mocker/packages/backend/src/shared/db/models/*.ts +TYPEORM_SYNCHRONIZE=true + +# API Server +PORT=3000 +NODE_ENV=development + +# External APIs (optional, for AI features) +OPENAI_API_KEY=sk-your-openai-key +GOOGLE_TRANSLATE_API_KEY=your-google-translate-key +``` + +#### Frontend (`packages/frontend/.env`) + +```bash +# Backend API URL +VITE_API_BASE_URL=http://localhost:3000 +``` + +### 4. Local Development Setup + +```bash +# Install dependencies (installs all workspaces) +npm install + +# Start backend development server +npm run start + +# In a new terminal, start frontend development server +npm run dev -w @mocker/frontend + +# Backend: http://localhost:3000 +# Frontend (search UI): http://localhost:5173 +``` + +### 5. Testing + +```bash +# Run all tests +npm run test + +# Run only backend tests +npm run test -w @mocker/backend + +# Run backend tests in watch mode +npm run test:watch -w @mocker/backend + +# Run with coverage +npm run test:coverage -w @mocker/backend +``` + +### 6. Linting & Formatting + +```bash +# Check linting and format issues +npm run lint +npm run format:check + +# Auto-fix linting and formatting issues +npm run lint:fix +npm run format:fix +``` + +### 7. Build for Production + +```bash +# Build all workspaces +npm run build + +# Build only backend +npm run build:backend + +# Build backend with production optimizations +npm run build:prod:backend + +# Build only frontend +npm run build:frontend ``` - TYPEORM_CONNECTION: mysql, - TYPEORM_HOST: localhost, - TYPEORM_PORT: 3306, - TYPEORM_USERNAME: , - TYPEORM_PASSWORD: , - TYPEORM_DATABASE: mockerdbdev, - TYPEORM_ENTITIES: /absolute/path/to/mocker/packages/backend/src/shared/db/models/*.ts, - TYPEORM_SYNCHRONIZE: true + +### 8. Docker + +```bash +# Build backend Docker image +docker build -f packages/backend/Dockerfile -t mocker-backend:latest . + +# Run Docker container +docker run -p 3000:3000 \ + -e TYPEORM_HOST=host.docker.internal \ + -e MUZZLE_BOT_TOKEN=xoxb-... \ + mocker-backend:latest + +# View logs +docker logs +docker logs | jq . ``` -3. `npm run start` (starts the backend server) +## Key Features + +### Slack Bot Commands + +- **Mock, Define, Muzzle, Counter, Confess, List, Walkie** - Various game modes and actions +- **Reputation Tracking** - Rep stats, reactions, achievements +- **Event Handling** - Real-time reactions, team join events, message history + +### Search & Auth System (New) + +- **OAuth Login** - Users authenticate via Slack to access the search UI +- **Team-Scoped Search** - Messages are filtered by `teamId` to prevent cross-workspace leakage +- **Session Tokens** - JWTs issued after OAuth callback, required for all search requests +- **Rate Limiting** - Auth endpoints: 20/15min, Search endpoints: 60/1min + +### AI Features (Optional) + +- **Daily Memory Job** - Summarizes conversations daily at 3 AM (requires OpenAI API key) +- **Sentiment Analysis** - Analyzes message tone +- **AI Summaries** - Generates summaries of message threads ### Scheduled Jobs From a1a23b2a44e948b2375087e3d6a54e2ce4f10f9e Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Fri, 27 Mar 2026 17:09:35 -0400 Subject: [PATCH 36/38] Added abortcontroller --- packages/frontend/src/App.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index f6c7ead7..4cbb36eb 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -22,6 +22,7 @@ export default function App() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [hasSearched, setHasSearched] = useState(false); + const abortControllerRef = useRef(null); const handleLogout = useCallback(() => { logout(() => { @@ -35,6 +36,15 @@ export default function App() { setIsLoading(true); setError(null); + // Abort any previous in-flight request to prevent stale results + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + // Create a new abort controller for this request + const abortController = new AbortController(); + abortControllerRef.current = abortController; + const params = new URLSearchParams(); if (userName.trim()) params.set('userName', userName.trim()); if (channel.trim()) params.set('channel', channel.trim()); @@ -44,6 +54,7 @@ export default function App() { const token = localStorage.getItem(AUTH_TOKEN_KEY) ?? ''; const response = await fetch(`${API_BASE_URL}/search/messages?${params.toString()}`, { headers: { Authorization: `Bearer ${token}` }, + signal: abortController.signal, }); if (response.status === 401) { handleLogout(); @@ -56,6 +67,10 @@ export default function App() { setMessages(data); setHasSearched(true); } catch (err) { + // Ignore abort errors from cancelled requests + if (err instanceof Error && err.name === 'AbortError') { + return; + } setError(err instanceof Error ? err.message : 'An unexpected error occurred'); } finally { setIsLoading(false); From 1b7e732adda4449fff9ca53f86604b1536dfebe6 Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Fri, 27 Mar 2026 17:19:09 -0400 Subject: [PATCH 37/38] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f4fb903a..b844ed3f 100644 --- a/README.md +++ b/README.md @@ -237,7 +237,7 @@ docker logs | jq . - **OAuth Login** - Users authenticate via Slack to access the search UI - **Team-Scoped Search** - Messages are filtered by `teamId` to prevent cross-workspace leakage -- **Session Tokens** - JWTs issued after OAuth callback, required for all search requests +- **Session Tokens** - HMAC-signed custom session tokens (base64url payload + signature, not JWT), issued after OAuth callback, required for all search requests - **Rate Limiting** - Auth endpoints: 20/15min, Search endpoints: 60/1min ### AI Features (Optional) From 63a805f819132aa64dbf7c707a8deb85fe9b8ea7 Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Fri, 27 Mar 2026 17:19:32 -0400 Subject: [PATCH 38/38] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b844ed3f..a10a6521 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Add these slash commands with their request URLs: - `reactions:read` - `users.profile:read` - `users:read` - - `users.identity` (for OAuth login flow) + - `identity.basic` (user token scope for OAuth login flow) Copy your **Bot Token** and **User OAuth Token** from the app credentials page.