From 3aea41f8624ea9551d435620de570a05d5b7f388 Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Thu, 21 May 2026 09:49:20 +0200 Subject: [PATCH 1/3] feat: add ESLint, lint-staged, and update pre-commit hook + CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ESLint 9 flat config (eslint.config.mjs) with typescript-eslint recommended rules and eslint-config-prettier - Add lint-staged to run eslint+prettier only on staged files - Update pre-commit hook to use lint-staged instead of full prettier check - Add `lint` and `format:check` scripts to package.json - Add Lint step to CI workflow - Fix resulting lint errors: unused vars (_ctx, _options, catch binding), any→unknown in type declarations, stale eslint-disable comments Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 5 +- .husky/pre-commit | 1 + eslint.config.mjs | 22 + package-lock.json | 1714 ++++++++++++++++++++++++++++++++++- package.json | 18 +- src/lib/cloudflare-email.ts | 2 +- src/routes/admin.ts | 36 +- src/test/setup.ts | 23 +- src/types/index.ts | 2 +- src/types/rss.d.ts | 4 +- src/utils/email-parser.ts | 4 +- 11 files changed, 1799 insertions(+), 32 deletions(-) create mode 100644 .husky/pre-commit create mode 100644 eslint.config.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ebd8b8..dd37416 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,10 @@ jobs: - run: npm ci - name: Format check - run: npx prettier --check '**/*.{js,ts,css,json,md}' + run: npm run format:check + + - name: Lint + run: npm run lint - name: Type check run: npm run typecheck diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..57fb103 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged && npm run typecheck && npm test diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..0d39a07 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,22 @@ +import prettier from "eslint-config-prettier"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist/", "coverage/"] }, + ...tseslint.configs.recommended, + prettier, + { + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + }, + }, + { + files: ["src/**/*.test.ts", "src/test/**/*.ts"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + }, + }, +); diff --git a/package-lock.json b/package-lock.json index 5fbd295..9d6f2d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,10 +19,14 @@ "@types/mailparser": "^3.4.6", "@types/rss": "^0.0.32", "@vitest/coverage-v8": "^4.0.18", + "eslint-config-prettier": "^10.1.8", "happy-dom": "^20.5.0", + "husky": "^9.1.7", + "lint-staged": "^17.0.5", "msw": "^2.12.8", "prettier": "^3.8.1", "typescript": "^5.9.3", + "typescript-eslint": "^8.59.4", "vitest": "^4.0.18", "wrangler": "^4.63.0" } @@ -682,6 +686,189 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@img/colour": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", @@ -1758,6 +1945,14 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1765,6 +1960,14 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/mailparser": { "version": "3.4.6", "resolved": "https://registry.npmjs.org/@types/mailparser/-/mailparser-3.4.6.tgz", @@ -1817,6 +2020,236 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", + "integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/type-utils": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.4", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz", + "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", + "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.4", + "@typescript-eslint/types": "^8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", + "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", + "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz", + "integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", + "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", + "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.4", + "@typescript-eslint/tsconfig-utils": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz", + "integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", + "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@vitest/coverage-v8": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", @@ -1959,6 +2392,49 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1998,6 +2474,19 @@ "node": ">=8" } }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2020,6 +2509,16 @@ "js-tokens": "^10.0.0" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/blake3-wasm": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", @@ -2027,6 +2526,19 @@ "dev": true, "license": "MIT" }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -2037,6 +2549,85 @@ "node": ">=18" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", @@ -2130,6 +2721,48 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2160,6 +2793,19 @@ "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", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-stack-parser-es": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", @@ -2229,6 +2875,184 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.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", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -2239,6 +3063,24 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -2249,6 +3091,30 @@ "node": ">=12.0.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2280,6 +3146,61 @@ "pnpm": ">=10" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC", + "peer": true + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2305,6 +3226,33 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/graphql": { "version": "16.12.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", @@ -2366,6 +3314,22 @@ "dev": true, "license": "MIT" }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -2379,6 +3343,39 @@ "node": ">=0.10.0" } }, + "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", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2389,6 +3386,20 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-node-process": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", @@ -2396,6 +3407,14 @@ "dev": true, "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC", + "peer": true + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -2442,6 +3461,41 @@ "dev": true, "license": "MIT" }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -2452,6 +3506,285 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lint-staged": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.5.tgz", + "integrity": "sha512-d12yC+/e8RhBjZtaxZn71FyrgU/P5e+uAPifhCLwdosQZP/zamSdKRWDC30ocVIbzDKiFG1McHc/LUgB92GIPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "listr2": "^10.2.1", + "picomatch": "^4.0.4", + "string-argv": "^0.3.2", + "tinyexec": "^1.1.2" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=22.22.1" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + }, + "optionalDependencies": { + "yaml": "^2.8.4" + } + }, + "node_modules/listr2": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", + "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.2.0", + "eventemitter3": "^5.0.4", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^10.0.0" + }, + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", + "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2490,6 +3823,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/miniflare": { "version": "4.20260205.0", "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260205.0.tgz", @@ -2533,6 +3879,29 @@ } } }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/msw": { "version": "2.12.8", "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.8.tgz", @@ -2607,6 +3976,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -2618,6 +3994,41 @@ ], "license": "MIT" }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/outvariant": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", @@ -2625,6 +4036,62 @@ "dev": true, "license": "MIT" }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -2647,9 +4114,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -2694,6 +4161,17 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", @@ -2710,6 +4188,17 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2720,6 +4209,23 @@ "node": ">=0.10.0" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/rettime": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.10.1.tgz", @@ -2727,6 +4233,13 @@ "dev": true, "license": "MIT" }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", @@ -2843,6 +4356,31 @@ "@img/sharp-win32-x64": "0.34.5" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -2863,6 +4401,39 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2904,6 +4475,16 @@ "dev": true, "license": "MIT" }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2966,9 +4547,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", "dev": true, "license": "MIT", "engines": { @@ -3035,6 +4616,19 @@ "node": ">=16" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -3043,6 +4637,20 @@ "license": "0BSD", "optional": true }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-fest": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.3.tgz", @@ -3073,6 +4681,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.4.tgz", + "integrity": "sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.4", + "@typescript-eslint/parser": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/undici": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", @@ -3110,6 +4742,17 @@ "url": "https://github.com/sponsors/kettanaito" } }, + "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", + "peer": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -3273,6 +4916,23 @@ "node": ">=12" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -3290,6 +4950,17 @@ "node": ">=8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/workerd": { "version": "1.20260205.0", "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260205.0.tgz", @@ -3905,6 +5576,23 @@ "node": ">=10" } }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -3934,6 +5622,20 @@ "node": ">=12" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoctocolors-cjs": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", diff --git a/package.json b/package.json index 86758de..3240bcb 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,24 @@ "scripts": { "build": "wrangler deploy --dry-run --outdir=dist", "format": "prettier --write '**/*.{js,ts,css,json,md}'", + "format:check": "prettier --check '**/*.{js,ts,css,json,md}'", + "lint": "eslint src", "dev": "wrangler dev", "deploy": "wrangler deploy --env production", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "prepare": "husky" + }, + "lint-staged": { + "*.{ts,js}": [ + "eslint --fix", + "prettier --write" + ], + "*.{json,md,css}": [ + "prettier --write" + ] }, "author": "", "license": "MIT", @@ -20,10 +32,14 @@ "@types/mailparser": "^3.4.6", "@types/rss": "^0.0.32", "@vitest/coverage-v8": "^4.0.18", + "eslint-config-prettier": "^10.1.8", "happy-dom": "^20.5.0", + "husky": "^9.1.7", + "lint-staged": "^17.0.5", "msw": "^2.12.8", "prettier": "^3.8.1", "typescript": "^5.9.3", + "typescript-eslint": "^8.59.4", "vitest": "^4.0.18", "wrangler": "^4.63.0" }, diff --git a/src/lib/cloudflare-email.ts b/src/lib/cloudflare-email.ts index 2b51acf..a67d2fe 100644 --- a/src/lib/cloudflare-email.ts +++ b/src/lib/cloudflare-email.ts @@ -5,7 +5,7 @@ import { processEmail, RawAttachment } from "./email-processor"; export async function handleCloudflareEmail( message: ForwardableEmailMessage, env: Env, - ctx: ExecutionContext, + _ctx: ExecutionContext, ): Promise { try { const email = await PostalMime.parse(message.raw); diff --git a/src/routes/admin.ts b/src/routes/admin.ts index e3efa8d..d6b384e 100644 --- a/src/routes/admin.ts +++ b/src/routes/admin.ts @@ -71,9 +71,11 @@ function timingSafeEqual(a: string, b: string): boolean { const aBytes = enc.encode(a); const bBytes = enc.encode(b); // Try native timing-safe implementation first (Cloudflare Workers runtime) - if (typeof (crypto.subtle as any).timingSafeEqual === "function") { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const subtle = crypto.subtle as any; + if (typeof subtle.timingSafeEqual === "function") { if (aBytes.length !== bBytes.length) return false; - return (crypto.subtle as any).timingSafeEqual(aBytes, bBytes); + return subtle.timingSafeEqual(aBytes, bBytes); } // Constant-time fallback for Node (test environment): encode length // mismatch into `diff` so the loop always runs over the full length. @@ -97,7 +99,9 @@ async function authMiddleware(c: Context, next: () => Promise) { // Proxy auth: only active when both env vars are present if (env.PROXY_AUTH_SECRET && env.PROXY_TRUSTED_IPS) { - const trustedIps = env.PROXY_TRUSTED_IPS.split(",").map((s) => s.trim()).filter(Boolean); + const trustedIps = env.PROXY_TRUSTED_IPS.split(",") + .map((s) => s.trim()) + .filter(Boolean); const clientIp = c.req.header("CF-Connecting-IP") ?? ""; const providedSecret = c.req.header("X-Auth-Proxy-Secret") ?? ""; const remoteUser = @@ -113,7 +117,11 @@ async function authMiddleware(c: Context, next: () => Promise) { } // Fallback: signed cookie - const authCookie = await getSignedCookie(c, env.ADMIN_PASSWORD, ADMIN_COOKIE_NAME); + const authCookie = await getSignedCookie( + c, + env.ADMIN_PASSWORD, + ADMIN_COOKIE_NAME, + ); if (authCookie !== "1") { return c.redirect("/admin/login"); } @@ -146,6 +154,7 @@ const authSchema = z.object({ }); // Base HTML layout with design system +// eslint-disable-next-line @typescript-eslint/no-explicit-any const layout = (title: string, content: any) => { return html` @@ -1610,11 +1619,14 @@ app.post("/feeds/create", async (c) => { if (isJson) { const body = await c.req.json>(); title = String(body.title ?? ""); - description = body.description != null ? String(body.description) : undefined; + description = + body.description != null ? String(body.description) : undefined; language = String(body.language ?? "en"); view = "list"; allowedSenders = Array.isArray(body.allowedSenders) - ? normalizeAllowedSenders((body.allowedSenders as unknown[]).map(String)) + ? normalizeAllowedSenders( + (body.allowedSenders as unknown[]).map(String), + ) : []; } else { const formData = await c.req.formData(); @@ -1930,8 +1942,9 @@ async function purgeFeedKeysStep( }); if (emailKeys.length > 0) { const emailDataResults = await Promise.allSettled( - emailKeys.map((k) => - emailStorage.get(k, { type: "json" }) as Promise, + emailKeys.map( + (k) => + emailStorage.get(k, { type: "json" }) as Promise, ), ); const attachmentIds = emailDataResults @@ -1976,7 +1989,12 @@ app.post("/feeds/:feedId/delete", async (c) => { // Best-effort cleanup in the background so the request stays fast. // Use the UI purge endpoint for full, user-visible progress. - waitUntilSafe(c, purgeFeedKeysStep(emailStorage, feedId, { bucket: env.ATTACHMENT_BUCKET })); + waitUntilSafe( + c, + purgeFeedKeysStep(emailStorage, feedId, { + bucket: env.ATTACHMENT_BUCKET, + }), + ); if (wantsJson) { return c.json({ ok: true, feedId }); } diff --git a/src/test/setup.ts b/src/test/setup.ts index 3f281cb..c5c586e 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -71,7 +71,7 @@ class MockCache implements Cache { async match( request: RequestInfo, - options?: CacheQueryOptions, + _options?: CacheQueryOptions, ): Promise { const key = request instanceof Request ? request.url : request; const response = this.store.get(key); @@ -80,7 +80,7 @@ class MockCache implements Cache { async delete( request: RequestInfo, - options?: CacheQueryOptions, + _options?: CacheQueryOptions, ): Promise { const key = request instanceof Request ? request.url : request; return this.store.delete(key); @@ -107,7 +107,6 @@ beforeAll(() => { server.listen({ onUnhandledRequest: "error" }); // Type-safe access to Node's global object for setting Workers-like globals in tests - // eslint-disable-next-line @typescript-eslint/no-explicit-any const g = globalThis as any; // Mock Cloudflare Workers runtime globals @@ -118,31 +117,26 @@ beforeAll(() => { // Mock crypto for generating random values if (!g.crypto) { - // eslint-disable-next-line @typescript-eslint/no-require-imports g.crypto = require("crypto").webcrypto; } // Ensure other required globals are available if (!g.FormData) { - // eslint-disable-next-line @typescript-eslint/no-require-imports const { FormData } = require("undici"); g.FormData = FormData; } if (!g.Headers) { - // eslint-disable-next-line @typescript-eslint/no-require-imports const { Headers } = require("undici"); g.Headers = Headers; } if (!g.Request) { - // eslint-disable-next-line @typescript-eslint/no-require-imports const { Request } = require("undici"); g.Request = Request; } if (!g.Response) { - // eslint-disable-next-line @typescript-eslint/no-require-imports const { Response } = require("undici"); g.Response = Response; } @@ -173,7 +167,13 @@ export class MockR2 { async put( key: string, - value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, + value: + | ReadableStream + | ArrayBuffer + | ArrayBufferView + | string + | null + | Blob, options?: { httpMetadata?: { contentType?: string; contentDisposition?: string }; }, @@ -216,7 +216,10 @@ export class MockR2 { headers.set("Content-Type", entry.httpMetadata.contentType); } if (entry.httpMetadata?.contentDisposition) { - headers.set("Content-Disposition", entry.httpMetadata.contentDisposition); + headers.set( + "Content-Disposition", + entry.httpMetadata.contentDisposition, + ); } }, }; diff --git a/src/types/index.ts b/src/types/index.ts index 4200e82..7bc7b86 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -71,7 +71,7 @@ declare global { // This is not an ideal solution but works for our example interface KVNamespace { get(key: string, options?: { type: "text" }): Promise; - get(key: string, options: { type: "json" }): Promise; + get(key: string, options: { type: "json" }): Promise; get( key: string, options: { type: "arrayBuffer" }, diff --git a/src/types/rss.d.ts b/src/types/rss.d.ts index a3ca0fc..2923c9c 100644 --- a/src/types/rss.d.ts +++ b/src/types/rss.d.ts @@ -15,7 +15,7 @@ declare module "rss" { generator?: string; categories?: string[]; custom_namespaces?: Record; - custom_elements?: any[]; + custom_elements?: unknown[]; } interface RSSItemOptions { @@ -34,7 +34,7 @@ declare module "rss" { size?: number; type?: string; }; - custom_elements?: any[]; + custom_elements?: unknown[]; } interface RSSXMLOptions { diff --git a/src/utils/email-parser.ts b/src/utils/email-parser.ts index ebd8478..0781495 100644 --- a/src/utils/email-parser.ts +++ b/src/utils/email-parser.ts @@ -7,6 +7,7 @@ export class EmailParser { return match ? match[1] : null; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any static parseForwardEmailPayload(payload: any): EmailData { if (!payload) { throw new Error("Missing or invalid webhook payload"); @@ -30,6 +31,7 @@ export class EmailParser { }; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any private static extractHeaders(payload: any): Record { const headers: Record = {}; @@ -62,7 +64,7 @@ export class EmailParser { if (encoding.toUpperCase() === "B") { try { return atob(text); - } catch (e) { + } catch { return text; } } else if (encoding.toUpperCase() === "Q") { From b24ee969d11d51115dfd84f5814310cb5d23e2ff Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Thu, 21 May 2026 11:35:37 +0200 Subject: [PATCH 2/3] style: fix Prettier formatting on 11 files Co-Authored-By: Claude Sonnet 4.6 --- README.md | 29 ++++--- src/lib/email-processor.test.ts | 78 +++++++++++++++---- src/lib/email-processor.ts | 13 +++- src/lib/forwardemail.ts | 14 ++-- src/routes/admin.test.ts | 8 +- src/routes/atom.test.ts | 6 +- src/routes/entries.ts | 72 +++++++++++------ src/routes/files.test.ts | 3 +- src/routes/inbound.ts | 5 +- src/utils/email-parser.test.ts | 28 ++++--- src/utils/feed-generator.test.ts | 128 ++++++++++++++++++++++++++----- 11 files changed, 289 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index df3a5d4..063719e 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ Email-to-RSS keeps the same workflow while avoiding shared domains and shared da Two ingestion methods are supported — pick one or use both: -| Method | How it works | -| ---------------------- | ------------------------------------------------------------------ | -| **Cloudflare Email Workers** | Cloudflare Email Routing delivers the raw message directly to the Worker via the `email()` handler — no outbound webhook needed | -| **ForwardEmail webhook** | ForwardEmail parses the message and POSTs a JSON payload to `POST /api/inbound`; the Worker verifies the source IP before processing | +| Method | How it works | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| **Cloudflare Email Workers** | Cloudflare Email Routing delivers the raw message directly to the Worker via the `email()` handler — no outbound webhook needed | +| **ForwardEmail webhook** | ForwardEmail parses the message and POSTs a JSON payload to `POST /api/inbound`; the Worker verifies the source IP before processing | Common path: @@ -60,7 +60,7 @@ Main routes: ## Cloudflare setup -If your domain is not yet on Cloudflare: in the [Cloudflare dashboard](https://dash.cloudflare.com/), go to *Add a site*, enter your domain, choose the Free plan, and follow the instructions to update your nameservers at your registrar. Wait for the zone to become active (usually a few minutes). +If your domain is not yet on Cloudflare: in the [Cloudflare dashboard](https://dash.cloudflare.com/), go to _Add a site_, enter your domain, choose the Free plan, and follow the instructions to update your nameservers at your registrar. Wait for the zone to become active (usually a few minutes). ## Setup @@ -70,9 +70,11 @@ If your domain is not yet on Cloudflare: in the [Cloudflare dashboard](https://d npx wrangler login ``` 3. Run setup: + ```bash bash setup.sh ``` + The script will prompt for an admin password and your domain, then: - install npm dependencies - verify Cloudflare auth (`wrangler whoami`) @@ -86,8 +88,8 @@ If your domain is not yet on Cloudflare: in the [Cloudflare dashboard](https://d No third-party service required. Cloudflare receives the email and hands it directly to the Worker. -1. In the Cloudflare dashboard, go to *Email → Email Routing* for your zone and click **Enable Email Routing**. Cloudflare will prompt you to add MX and SPF records — accept and it adds them automatically. -2. Under *Email Routing → Routing Rules*, add a **Catch-all** rule: +1. In the Cloudflare dashboard, go to _Email → Email Routing_ for your zone and click **Enable Email Routing**. Cloudflare will prompt you to add MX and SPF records — accept and it adds them automatically. +2. Under _Email Routing → Routing Rules_, add a **Catch-all** rule: - Action: **Send to Worker** - Worker: `email-to-rss` (the name from `wrangler.toml`) @@ -97,7 +99,7 @@ That's it. No webhook configuration is needed. Use this if you prefer ForwardEmail's additional features (sender filtering, open-tracking, etc.). -Add these DNS records in Cloudflare (*DNS → Records*): +Add these DNS records in Cloudflare (_DNS → Records_): | Type | Name | Content | Notes | | ---- | ---- | ---------------------------------------------------- | ----------------------- | @@ -111,14 +113,16 @@ Replace `yourdomain.com` with your actual domain. The Worker verifies each webhook request against ForwardEmail's published MX IP list before processing it. 5. Deploy: + ```bash npm run deploy ``` + Wrangler will create the Worker and register `yourdomain.com` (and `www.yourdomain.com`) as custom domains pointing to it. Cloudflare handles TLS automatically. 6. Open `https://yourdomain.com/admin` and sign in. -> **Tip:** To verify the Worker is running, check *Workers & Pages → email-to-rss* in the Cloudflare dashboard. The *Custom Domains* tab should list your domain once the deploy succeeds. +> **Tip:** To verify the Worker is running, check _Workers & Pages → email-to-rss_ in the Cloudflare dashboard. The _Custom Domains_ tab should list your domain once the deploy succeeds. ## Development @@ -153,7 +157,7 @@ This feature is **optional**. If no R2 bucket is bound, attachments are silently **Setup:** -1. Create an R2 bucket in the Cloudflare dashboard (*R2 Object Storage → Create bucket*), or with Wrangler: +1. Create an R2 bucket in the Cloudflare dashboard (_R2 Object Storage → Create bucket_), or with Wrangler: ```bash npx wrangler r2 bucket create your-bucket-name ``` @@ -177,8 +181,8 @@ Instead of the built-in password login you can delegate admin authentication to **Required Worker secrets** (set with `wrangler secret put`, never in `[vars]`): -| Secret | Description | -|---|---| +| Secret | Description | +| ------------------- | ---------------------------------------------- | | `PROXY_AUTH_SECRET` | Shared secret between the proxy and the Worker | **Required `[vars]`** in `wrangler.toml`: @@ -188,6 +192,7 @@ PROXY_TRUSTED_IPS = "10.0.0.1" # comma-separated IPs of your reverse proxy ``` When both are configured, the Worker authenticates a request if: + 1. `CF-Connecting-IP` is in `PROXY_TRUSTED_IPS` 2. The `X-Auth-Proxy-Secret` header matches `PROXY_AUTH_SECRET` 3. `Remote-User` or `X-Forwarded-User` is non-empty diff --git a/src/lib/email-processor.test.ts b/src/lib/email-processor.test.ts index ac7163d..e7d7b58 100644 --- a/src/lib/email-processor.test.ts +++ b/src/lib/email-processor.test.ts @@ -1,7 +1,11 @@ import { describe, it, expect, beforeEach } from "vitest"; import "../test/setup"; import { createMockEnv, MockR2 } from "../test/setup"; -import { processEmail, ProcessEmailInput, RawAttachment } from "./email-processor"; +import { + processEmail, + ProcessEmailInput, + RawAttachment, +} from "./email-processor"; const VALID_FEED_ID = "apple.mountain.42"; const VALID_TO = `${VALID_FEED_ID}@test.getmynews.app`; @@ -139,25 +143,53 @@ describe("processEmail", () => { const oldKey1 = `feed:${VALID_FEED_ID}:111`; const oldKey2 = `feed:${VALID_FEED_ID}:222`; const bigContent = "x".repeat(200); - const email1 = JSON.stringify({ subject: "Old1", from: "a@b.com", content: bigContent, receivedAt: 111, headers: {} }); - const email2 = JSON.stringify({ subject: "Old2", from: "a@b.com", content: bigContent, receivedAt: 222, headers: {} }); + const email1 = JSON.stringify({ + subject: "Old1", + from: "a@b.com", + content: bigContent, + receivedAt: 111, + headers: {}, + }); + const email2 = JSON.stringify({ + subject: "Old2", + from: "a@b.com", + content: bigContent, + receivedAt: 222, + headers: {}, + }); await env.EMAIL_STORAGE.put(oldKey1, email1); await env.EMAIL_STORAGE.put(oldKey2, email2); await env.EMAIL_STORAGE.put( `feed:${VALID_FEED_ID}:metadata`, JSON.stringify({ emails: [ - { key: oldKey2, subject: "Old2", receivedAt: 222, size: email2.length }, - { key: oldKey1, subject: "Old1", receivedAt: 111, size: email1.length }, + { + key: oldKey2, + subject: "Old2", + receivedAt: 222, + size: email2.length, + }, + { + key: oldKey1, + subject: "Old1", + receivedAt: 111, + size: email1.length, + }, ], }), ); const tinyEnv = { ...env, FEED_MAX_SIZE_BYTES: "50" }; - const res = await processEmail(makeInput({ subject: "New" }), tinyEnv as any); + const res = await processEmail( + makeInput({ subject: "New" }), + tinyEnv as any, + ); expect(res.status).toBe(200); - const metadata = await env.EMAIL_STORAGE.get(`feed:${VALID_FEED_ID}:metadata`, "json"); + const metadata = await env.EMAIL_STORAGE.get( + `feed:${VALID_FEED_ID}:metadata`, + "json", + ); expect(metadata.emails).toHaveLength(1); expect(metadata.emails[0].subject).toBe("New"); @@ -175,13 +207,17 @@ describe("processEmail", () => { const bigEnv = { ...env, FEED_MAX_SIZE_BYTES: String(10 * 1024 * 1024) }; await processEmail(makeInput({ subject: "First" }), bigEnv as any); await processEmail(makeInput({ subject: "Second" }), bigEnv as any); - const metadata = await env.EMAIL_STORAGE.get(`feed:${VALID_FEED_ID}:metadata`, "json"); + const metadata = await env.EMAIL_STORAGE.get( + `feed:${VALID_FEED_ID}:metadata`, + "json", + ); expect(metadata.emails).toHaveLength(2); }); }); describe("processEmail — attachments", () => { - const pdfContent = new TextEncoder().encode("PDF bytes").buffer as ArrayBuffer; + const pdfContent = new TextEncoder().encode("PDF bytes") + .buffer as ArrayBuffer; const pdfAttachment: RawAttachment = { filename: "report.pdf", @@ -205,7 +241,10 @@ describe("processEmail — attachments", () => { `feed:${VALID_FEED_ID}:metadata`, "json", ); - const emailData = await env.EMAIL_STORAGE.get(metadata.emails[0].key, "json"); + const emailData = await env.EMAIL_STORAGE.get( + metadata.emails[0].key, + "json", + ); expect(emailData.attachments).toBeUndefined(); }); @@ -226,7 +265,10 @@ describe("processEmail — attachments", () => { `feed:${VALID_FEED_ID}:metadata`, "json", ); - const emailData = await env.EMAIL_STORAGE.get(metadata.emails[0].key, "json"); + const emailData = await env.EMAIL_STORAGE.get( + metadata.emails[0].key, + "json", + ); expect(emailData.attachments).toHaveLength(1); expect(emailData.attachments[0].filename).toBe("report.pdf"); @@ -271,7 +313,14 @@ describe("processEmail — attachments", () => { content: bigContent, receivedAt: 111, headers: {}, - attachments: [{ id: oldAttachmentId, filename: "old.pdf", contentType: "application/pdf", size: 100 }], + attachments: [ + { + id: oldAttachmentId, + filename: "old.pdf", + contentType: "application/pdf", + size: 100, + }, + ], }); await env.EMAIL_STORAGE.put(oldKey, oldEmail); @@ -295,7 +344,10 @@ describe("processEmail — attachments", () => { // Process with tight size budget to force trimming const tinyEnv = { ...env, FEED_MAX_SIZE_BYTES: "50" }; - const res = await processEmail(makeInput({ subject: "New" }), tinyEnv as any); + const res = await processEmail( + makeInput({ subject: "New" }), + tinyEnv as any, + ); expect(res.status).toBe(200); // Old attachment should be deleted from R2 diff --git a/src/lib/email-processor.ts b/src/lib/email-processor.ts index d94f281..2a2ae97 100644 --- a/src/lib/email-processor.ts +++ b/src/lib/email-processor.ts @@ -1,5 +1,11 @@ import { EmailParser } from "../utils/email-parser"; -import { AttachmentData, EmailMetadata, Env, FeedConfig, FeedMetadata } from "../types"; +import { + AttachmentData, + EmailMetadata, + Env, + FeedConfig, + FeedMetadata, +} from "../types"; export interface RawAttachment { filename: string; @@ -151,7 +157,10 @@ export async function processEmail( }; feedMetadata.emails.unshift(newEntry); - let totalSize = feedMetadata.emails.reduce((sum, e) => sum + (e.size ?? 0), 0); + let totalSize = feedMetadata.emails.reduce( + (sum, e) => sum + (e.size ?? 0), + 0, + ); const toDelete: EmailMetadata[] = []; while (totalSize > maxBytes && feedMetadata.emails.length > 1) { const dropped = feedMetadata.emails.pop()!; diff --git a/src/lib/forwardemail.ts b/src/lib/forwardemail.ts index 5488938..2036f33 100644 --- a/src/lib/forwardemail.ts +++ b/src/lib/forwardemail.ts @@ -6,10 +6,7 @@ export interface ForwardEmailAttachment { filename?: string; contentType?: string; size?: number; - content?: - | { type: "Buffer"; data: number[] } - | ArrayBuffer - | ArrayBufferView; + content?: { type: "Buffer"; data: number[] } | ArrayBuffer | ArrayBufferView; } export interface ForwardEmailPayload { @@ -56,8 +53,13 @@ function toArrayBuffer( ): ArrayBuffer | null { if (!content) return null; if (content instanceof ArrayBuffer) return content; - if (ArrayBuffer.isView(content)) return (content as ArrayBufferView).buffer as ArrayBuffer; - if (typeof content === "object" && content.type === "Buffer" && Array.isArray(content.data)) { + if (ArrayBuffer.isView(content)) + return (content as ArrayBufferView).buffer as ArrayBuffer; + if ( + typeof content === "object" && + content.type === "Buffer" && + Array.isArray(content.data) + ) { return Uint8Array.from(content.data).buffer as ArrayBuffer; } return null; diff --git a/src/routes/admin.test.ts b/src/routes/admin.test.ts index 0a31e3a..a3f3289 100644 --- a/src/routes/admin.test.ts +++ b/src/routes/admin.test.ts @@ -14,7 +14,8 @@ describe("Admin Routes", () => { mockEnv = createMockEnv() as unknown as Env; testApp = new Hono(); testApp.route("/admin", app); - request = (path, init = {}) => Promise.resolve(testApp.request(path, init, mockEnv)); + request = (path, init = {}) => + Promise.resolve(testApp.request(path, init, mockEnv)); loginAndGetCookie = async () => { const formData = new FormData(); formData.append("password", "test-password"); @@ -366,7 +367,10 @@ describe("Admin Routes", () => { } as unknown as Env; } - function makeProxyRequest(path: string, headers: Record = {}) { + function makeProxyRequest( + path: string, + headers: Record = {}, + ) { const proxyApp = new Hono(); proxyApp.route("/admin", app); return proxyApp.request(path, { headers }, proxyEnv()); diff --git a/src/routes/atom.test.ts b/src/routes/atom.test.ts index d587a11..959cb43 100644 --- a/src/routes/atom.test.ts +++ b/src/routes/atom.test.ts @@ -68,7 +68,11 @@ describe("Atom Feed Route", () => { `feed:${FEED_ID}:metadata`, JSON.stringify({ emails: [ - { key: emailKey, subject: "Atom Entry Subject", receivedAt: 1700000001000 }, + { + key: emailKey, + subject: "Atom Entry Subject", + receivedAt: 1700000001000, + }, ], }), ); diff --git a/src/routes/entries.ts b/src/routes/entries.ts index a2c969b..438da6c 100644 --- a/src/routes/entries.ts +++ b/src/routes/entries.ts @@ -21,7 +21,9 @@ export async function handle(c: Context): Promise { return new Response("Feed not found", { status: 404 }); } - const metaEntry = feedMetadata.emails.find((e) => e.receivedAt === receivedAt); + const metaEntry = feedMetadata.emails.find( + (e) => e.receivedAt === receivedAt, + ); if (!metaEntry) { return new Response("Entry not found", { status: 404 }); } @@ -39,26 +41,50 @@ export async function handle(c: Context): Promise { "default-src 'none'; style-src 'unsafe-inline'; img-src *; frame-src 'none'", ); - return c.html(html` - - - - - ${emailData.subject} - - - -

${emailData.subject}

-
-
From:
${emailData.from}
-
Date:
${new Date(emailData.receivedAt).toUTCString()}
-
-
${raw(emailData.content)}
- -`); + return c.html( + html` + + + + + ${emailData.subject} + + + +

${emailData.subject}

+
+
From:
+
${emailData.from}
+
Date:
+
${new Date(emailData.receivedAt).toUTCString()}
+
+
${raw(emailData.content)}
+ + `, + ); } diff --git a/src/routes/files.test.ts b/src/routes/files.test.ts index 0480362..dd20616 100644 --- a/src/routes/files.test.ts +++ b/src/routes/files.test.ts @@ -39,7 +39,8 @@ describe("GET /files/:attachmentId/:filename", () => { }); it("returns 200 with stored content when attachment exists", async () => { - const content = new TextEncoder().encode("PDF content").buffer as ArrayBuffer; + const content = new TextEncoder().encode("PDF content") + .buffer as ArrayBuffer; await mockR2.put("test-uuid", content, { httpMetadata: { contentType: "application/pdf" }, }); diff --git a/src/routes/inbound.ts b/src/routes/inbound.ts index e62d7ea..cc0d69b 100644 --- a/src/routes/inbound.ts +++ b/src/routes/inbound.ts @@ -1,9 +1,6 @@ import { Context } from "hono"; import { Env } from "../types"; -import { - ForwardEmailPayload, - handleForwardEmail, -} from "../lib/forwardemail"; +import { ForwardEmailPayload, handleForwardEmail } from "../lib/forwardemail"; export async function handle(c: Context): Promise { try { diff --git a/src/utils/email-parser.test.ts b/src/utils/email-parser.test.ts index d250586..2b76981 100644 --- a/src/utils/email-parser.test.ts +++ b/src/utils/email-parser.test.ts @@ -53,15 +53,15 @@ describe("EmailParser.decodeEncodedWords", () => { // =?UTF-8?Q?caf=C3=A9?= → "café" (but decodeQuotedPrintable works byte-by-byte) // Use a simple ASCII QP sequence to stay charset-agnostic in tests // =?US-ASCII?Q?Hello=20World?= → "Hello World" (=20 → space, _ → space) - expect( - EmailParser.decodeEncodedWords("=?US-ASCII?Q?Hello=20World?="), - ).toBe("Hello World"); + expect(EmailParser.decodeEncodedWords("=?US-ASCII?Q?Hello=20World?=")).toBe( + "Hello World", + ); }); it("decodes underscores as spaces in QP encoding", () => { - expect( - EmailParser.decodeEncodedWords("=?US-ASCII?Q?Hello_World?="), - ).toBe("Hello World"); + expect(EmailParser.decodeEncodedWords("=?US-ASCII?Q?Hello_World?=")).toBe( + "Hello World", + ); }); it("leaves unrecognised encoded-word syntax unchanged", () => { @@ -79,9 +79,7 @@ describe("EmailParser.parseForwardEmailPayload", () => { }); it("throws on undefined payload", () => { - expect(() => - EmailParser.parseForwardEmailPayload(undefined), - ).toThrow(); + expect(() => EmailParser.parseForwardEmailPayload(undefined)).toThrow(); }); it("parses subject, from, and HTML content", () => { @@ -95,7 +93,9 @@ describe("EmailParser.parseForwardEmailPayload", () => { expect(result.subject).toBe("Test Subject"); expect(result.from).toBe("sender@example.com"); expect(result.content).toBe("

Hello

"); - expect(result.receivedAt).toBe(new Date("2024-01-15T10:00:00.000Z").getTime()); + expect(result.receivedAt).toBe( + new Date("2024-01-15T10:00:00.000Z").getTime(), + ); }); it("prefers HTML content over plain text", () => { @@ -137,14 +137,18 @@ describe("EmailParser.parseForwardEmailPayload", () => { it("uses Date.now() when date field is absent", () => { const before = Date.now(); - const result = EmailParser.parseForwardEmailPayload({ from: { text: "x@y.com" } }); + const result = EmailParser.parseForwardEmailPayload({ + from: { text: "x@y.com" }, + }); const after = Date.now(); expect(result.receivedAt).toBeGreaterThanOrEqual(before); expect(result.receivedAt).toBeLessThanOrEqual(after); }); it("defaults subject to 'No Subject' when absent", () => { - const result = EmailParser.parseForwardEmailPayload({ from: { text: "x@y.com" } }); + const result = EmailParser.parseForwardEmailPayload({ + from: { text: "x@y.com" }, + }); expect(result.subject).toBe("No Subject"); }); diff --git a/src/utils/feed-generator.test.ts b/src/utils/feed-generator.test.ts index 8b80e00..3527492 100644 --- a/src/utils/feed-generator.test.ts +++ b/src/utils/feed-generator.test.ts @@ -38,13 +38,23 @@ const FEED_ID = "abc123"; describe("generateRssFeed", () => { it("returns RSS 2.0 with channel element", () => { - const result = generateRssFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateRssFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain(""); expect(result).toContain("Test Newsletter"); }); it("includes element for email with attachment", () => { - const result = generateRssFeed(mockFeedConfig, [mockEmailWithAttachment], BASE_URL, FEED_ID); + const result = generateRssFeed( + mockFeedConfig, + [mockEmailWithAttachment], + BASE_URL, + FEED_ID, + ); expect(result).toContain(" { }); it("does not include for email without attachments", () => { - const result = generateRssFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateRssFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).not.toContain(" { - const result = generateRssFeed(mockFeedConfig, [mockEmailWithAttachment], BASE_URL, FEED_ID); - expect(result).toContain(`${BASE_URL}/files/550e8400-e29b-41d4-a716-446655440000/report.pdf`); + const result = generateRssFeed( + mockFeedConfig, + [mockEmailWithAttachment], + BASE_URL, + FEED_ID, + ); + expect(result).toContain( + `${BASE_URL}/files/550e8400-e29b-41d4-a716-446655440000/report.pdf`, + ); }); it("includes rss self-link in RSS output", () => { - const result = generateRssFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateRssFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain(`${BASE_URL}/rss/${FEED_ID}`); }); it("includes email entries as elements", () => { - const result = generateRssFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateRssFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain(""); expect(result).toContain("Hello World"); }); @@ -82,39 +114,74 @@ describe("generateRssFeed", () => { describe("generateAtomFeed", () => { it("returns Atom 1.0 namespace", () => { - const result = generateAtomFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain('xmlns="http://www.w3.org/2005/Atom"'); }); it("contains root element", () => { - const result = generateAtomFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain(""); }); it("includes feed title", () => { - const result = generateAtomFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain("Test Newsletter"); }); it("includes elements for each email", () => { - const result = generateAtomFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain(""); expect(result).toContain("Hello World"); }); it("includes author information", () => { - const result = generateAtomFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain("Alice"); }); it("self-link points to atom URL", () => { - const result = generateAtomFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain(`${BASE_URL}/atom/${FEED_ID}`); }); it("includes rss alternate link", () => { - const result = generateAtomFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain(`${BASE_URL}/rss/${FEED_ID}`); }); @@ -125,26 +192,49 @@ describe("generateAtomFeed", () => { }); it("handles config without description", () => { - const configNoDesc: FeedConfig = { ...mockFeedConfig, description: undefined }; - const result = generateAtomFeed(configNoDesc, mockEmails, BASE_URL, FEED_ID); + const configNoDesc: FeedConfig = { + ...mockFeedConfig, + description: undefined, + }; + const result = generateAtomFeed( + configNoDesc, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain('xmlns="http://www.w3.org/2005/Atom"'); }); it("handles config with author field", () => { const configWithAuthor: FeedConfig = { ...mockFeedConfig, author: "Bob" }; - const result = generateAtomFeed(configWithAuthor, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + configWithAuthor, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain("Bob"); }); it("includes enclosure link for email with attachment in Atom feed", () => { - const result = generateAtomFeed(mockFeedConfig, [mockEmailWithAttachment], BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + [mockEmailWithAttachment], + BASE_URL, + FEED_ID, + ); expect(result).toContain('rel="enclosure"'); expect(result).toContain("550e8400-e29b-41d4-a716-446655440000"); expect(result).toContain("report.pdf"); }); it("does not include enclosure link for email without attachments in Atom feed", () => { - const result = generateAtomFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).not.toContain('rel="enclosure"'); }); }); From b26990a875914d97688f5f3d13c38f9f90619b4d Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Thu, 21 May 2026 12:09:26 +0200 Subject: [PATCH 3/3] fix: address PR review comments - Fix KV json overload to return Promise (null on missing keys) - Add shebang to Husky pre-commit hook - Explicitly add eslint ^10.0.0 to devDependencies Co-Authored-By: Claude Sonnet 4.6 --- .husky/pre-commit | 1 + package.json | 1 + src/types/index.ts | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 57fb103..1ac5081 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,2 @@ +#!/bin/sh npx lint-staged && npm run typecheck && npm test diff --git a/package.json b/package.json index 3240bcb..df540a9 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@types/mailparser": "^3.4.6", "@types/rss": "^0.0.32", "@vitest/coverage-v8": "^4.0.18", + "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "happy-dom": "^20.5.0", "husky": "^9.1.7", diff --git a/src/types/index.ts b/src/types/index.ts index 7bc7b86..92f2a08 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -71,7 +71,7 @@ declare global { // This is not an ideal solution but works for our example interface KVNamespace { get(key: string, options?: { type: "text" }): Promise; - get(key: string, options: { type: "json" }): Promise; + get(key: string, options: { type: "json" }): Promise; get( key: string, options: { type: "arrayBuffer" },