diff --git a/AGENTS.md b/AGENTS.md index b51bed607..d534224ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,6 +77,7 @@ The repository is organized as a monorepo with the following packages: - *packages/koa/*: Koa integration (@fedify/koa) - *packages/postgres/*: PostgreSQL drivers (@fedify/postgres) - *packages/redis/*: Redis drivers (@fedify/redis) + - *packages/lint/*: Linting utilities (@fedify/lint) - *packages/nestjs/*: NestJS integration (@fedify/nestjs) - *packages/next/*: Next.js integration (@fedify/next) - *packages/sqlite/*: SQLite driver (@fedify/sqlite) diff --git a/CHANGES.md b/CHANGES.md index 3b9ab3245..36e7c17dd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -186,6 +186,13 @@ To be released. - This package is primarily used by generated vocabulary classes and provides the runtime infrastructure for ActivityPub object processing. +### @fedify/lint + + - Created Fedify linting tools as the *@fedify/lint* package. + This package provides shared Deno Lint and ESLint configurations for + consistent code style across Fedify packages and user projects. + [[#297], [#494] by ChanHaeng Lee] + Version 1.10.0 -------------- diff --git a/cspell.json b/cspell.json index fbc4eb7d9..7702896ea 100644 --- a/cspell.json +++ b/cspell.json @@ -104,6 +104,7 @@ "traceparent", "ts-nocheck", "tsdown", + "TSES", "twoslash", "typeof", "unfollow", diff --git a/deno.json b/deno.json index 16911e216..4f2987653 100644 --- a/deno.json +++ b/deno.json @@ -10,6 +10,7 @@ "./packages/h3", "./packages/hono", "./packages/koa", + "./packages/lint", "./packages/postgres", "./packages/redis", "./packages/relay", @@ -23,6 +24,7 @@ "imports": { "@cloudflare/workers-types": "npm:@cloudflare/workers-types@^4.20250529.0", "@david/dax": "jsr:@david/dax@^0.43.2", + "@fxts/core": "npm:@fxts/core@^1.21.1", "@js-temporal/polyfill": "npm:@js-temporal/polyfill@^0.5.1", "@logtape/file": "jsr:@logtape/file@^1.2.2", "@logtape/logtape": "jsr:@logtape/logtape@^1.2.2", @@ -38,7 +40,7 @@ "@types/node": "npm:@types/node@^22.16.0", "amqplib": "npm:amqplib@^0.10.8", "byte-encodings": "npm:byte-encodings@^1.0.11", - "es-toolkit": "npm:es-toolkit@^1.39.10", + "es-toolkit": "npm:es-toolkit@^1.42.0", "h3": "npm:h3@^1.15.0", "ioredis": "npm:ioredis@^5.6.1", "json-preserve-indent": "npm:json-preserve-indent@^1.1.3", diff --git a/deno.lock b/deno.lock index 3c6ae2c75..ffa195ab2 100644 --- a/deno.lock +++ b/deno.lock @@ -5,14 +5,16 @@ "jsr:@david/dax@~0.43.2": "0.43.2", "jsr:@david/path@0.2": "0.2.0", "jsr:@david/which@~0.4.1": "0.4.1", - "jsr:@es-toolkit/es-toolkit@^1.39.5": "1.39.10", + "jsr:@es-toolkit/es-toolkit@^1.39.5": "1.43.0", "jsr:@hongminhee/localtunnel@0.3": "0.3.0", - "jsr:@hono/hono@^4.8.3": "4.10.2", - "jsr:@logtape/file@^1.2.2": "1.2.2", - "jsr:@logtape/logtape@^1.0.4": "1.2.2", - "jsr:@logtape/logtape@^1.2.2": "1.2.2", - "jsr:@optique/core@~0.6.1": "0.6.1", - "jsr:@optique/run@~0.6.1": "0.6.1", + "jsr:@hono/hono@^4.8.3": "4.11.1", + "jsr:@logtape/file@^1.2.2": "1.3.5", + "jsr:@logtape/logtape@^1.0.4": "1.3.5", + "jsr:@logtape/logtape@^1.2.2": "1.3.5", + "jsr:@logtape/logtape@^1.3.5": "1.3.5", + "jsr:@optique/core@~0.6.1": "0.6.5", + "jsr:@optique/core@~0.6.5": "0.6.5", + "jsr:@optique/run@~0.6.1": "0.6.5", "jsr:@std/bytes@^1.0.5": "1.0.6", "jsr:@std/fmt@1": "1.0.8", "jsr:@std/fs@1": "1.0.20", @@ -21,13 +23,12 @@ "jsr:@std/path@1": "1.1.3", "jsr:@std/path@^1.0.6": "1.1.3", "jsr:@std/path@^1.1.3": "1.1.3", - "jsr:@std/yaml@^1.0.8": "1.0.10", "npm:@alinea/suite@~0.6.3": "0.6.3", "npm:@cfworker/json-schema@^4.1.1": "4.1.1", - "npm:@cloudflare/workers-types@^4.20250529.0": "4.20251213.0", - "npm:@fxts/core@^1.15.0": "1.21.1", + "npm:@cloudflare/workers-types@^4.20250529.0": "4.20251219.0", + "npm:@fxts/core@^1.21.1": "1.21.1", "npm:@hongminhee/localtunnel@0.3": "0.3.0", - "npm:@inquirer/prompts@^7.8.4": "7.10.1_@types+node@22.19.2", + "npm:@inquirer/prompts@^7.8.4": "7.10.1_@types+node@22.19.3", "npm:@jimp/core@^1.6.0": "1.6.0", "npm:@jimp/wasm-webp@^1.6.0": "1.6.0", "npm:@js-temporal/polyfill@~0.5.1": "0.5.1", @@ -40,10 +41,14 @@ "npm:@optique/core@~0.6.1": "0.6.5", "npm:@optique/run@~0.6.1": "0.6.5", "npm:@poppanator/http-constants@^1.1.1": "1.1.1", - "npm:@sveltejs/kit@2": "2.49.2_@opentelemetry+api@1.9.0_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.46.0___acorn@8.15.0__vite@7.2.7___@types+node@22.19.2___tsx@4.21.0___yaml@2.8.2___picomatch@4.0.3__@types+node@22.19.2__tsx@4.21.0__yaml@2.8.2_svelte@5.46.0__acorn@8.15.0_vite@7.2.7__@types+node@22.19.2__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_acorn@8.15.0_@types+node@22.19.2_tsx@4.21.0_yaml@2.8.2", + "npm:@sveltejs/kit@2": "2.49.2_@opentelemetry+api@1.9.0_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.46.0___acorn@8.15.0__vite@7.3.0___@types+node@22.19.3___tsx@4.21.0___yaml@2.8.2___picomatch@4.0.3__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2_svelte@5.46.0__acorn@8.15.0_vite@7.3.0__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_acorn@8.15.0_@types+node@22.19.3_tsx@4.21.0_yaml@2.8.2", "npm:@types/amqplib@~0.10.7": "0.10.8", - "npm:@types/node@^22.16.0": "22.19.2", - "npm:@types/node@^24.2.1": "24.10.3", + "npm:@types/eslint@9": "9.6.1", + "npm:@types/estree@^1.0.8": "1.0.8", + "npm:@types/node@^22.16.0": "22.19.3", + "npm:@types/node@^24.2.1": "24.10.4", + "npm:@typescript-eslint/parser@^8.49.0": "8.50.0_eslint@9.39.2_typescript@5.9.3", + "npm:@typescript-eslint/utils@8": "8.50.0_eslint@9.39.2_typescript@5.9.3", "npm:amqplib@~0.10.8": "0.10.9", "npm:asn1js@^3.0.5": "3.0.7", "npm:asn1js@^3.0.6": "3.0.7", @@ -57,6 +62,8 @@ "npm:es-toolkit@^1.31.0": "1.43.0", "npm:es-toolkit@^1.39.10": "1.43.0", "npm:es-toolkit@^1.39.5": "1.43.0", + "npm:es-toolkit@^1.42.0": "1.43.0", + "npm:eslint@9": "9.39.2", "npm:express@4": "4.22.1", "npm:fast-check@^3.22.0": "3.23.2", "npm:fastify-plugin@^5.0.1": "5.1.0", @@ -64,17 +71,17 @@ "npm:fetch-mock@^12.5.2": "12.6.0", "npm:fetch-mock@^12.5.4": "12.6.0", "npm:h3@^1.15.0": "1.15.4", - "npm:hono@^4.8.3": "4.11.0", + "npm:hono@^4.8.3": "4.11.1", "npm:icojs@~0.19.5": "0.19.5", "npm:inquirer-toggle@^1.0.1": "1.0.1", - "npm:inquirer@^12.9.4": "12.11.1_@types+node@22.19.2", + "npm:inquirer@^12.9.4": "12.11.1_@types+node@22.19.3", "npm:ioredis@^5.6.1": "5.8.2", "npm:jimp@^1.6.0": "1.6.0", "npm:json-canon@^1.0.1": "1.0.1", "npm:json-preserve-indent@^1.1.3": "1.1.3", "npm:jsonld@9": "9.0.0", "npm:koa@2": "2.16.3", - "npm:miniflare@^4.20250523.0": "4.20251210.0", + "npm:miniflare@^4.20250523.0": "4.20251217.0", "npm:multicodec@^3.2.1": "3.2.1", "npm:ora@^8.2.0": "8.2.0", "npm:pkijs@^3.2.4": "3.3.3", @@ -84,12 +91,12 @@ "npm:shiki@^1.6.4": "1.29.2", "npm:srvx@~0.8.7": "0.8.16", "npm:structured-field-values@^2.0.4": "2.0.4", - "npm:tsdown@~0.12.9": "0.12.9_rolldown@1.0.0-beta.54", + "npm:tsdown@~0.12.9": "0.12.9_rolldown@1.0.0-beta.55", "npm:tsx@^4.19.4": "4.21.0", "npm:uri-template-router@1": "1.0.0", "npm:url-template@^3.1.1": "3.1.1", "npm:urlpattern-polyfill@^10.1.0": "10.1.0", - "npm:wrangler@^4.17.0": "4.54.0_@cloudflare+workers-types@4.20251213.0_unenv@2.0.0-rc.24_workerd@1.20251210.0", + "npm:wrangler@^4.17.0": "4.56.0_@cloudflare+workers-types@4.20251219.0_unenv@2.0.0-rc.24_workerd@1.20251217.0", "npm:yaml@^2.8.1": "2.8.2" }, "jsr": { @@ -118,8 +125,8 @@ "@david/which@0.4.1": { "integrity": "896a682b111f92ab866cc70c5b4afab2f5899d2f9bde31ed00203b9c250f225e" }, - "@es-toolkit/es-toolkit@1.39.10": { - "integrity": "8757072a13aa64b3b349ba2b9d7d22fbe7ea6f138506c6cd2222d767cd79918f" + "@es-toolkit/es-toolkit@1.43.0": { + "integrity": "ce62274cd14ad9317290b5d77e2c2bf752b78a96e393c0306640f59753e7e043" }, "@hongminhee/localtunnel@0.3.0": { "integrity": "4ad858acd963b5fad45b188d54cf751ac8fbe0aae495e1d3ce607e3730270ff4", @@ -127,25 +134,25 @@ "jsr:@logtape/logtape@^1.0.4" ] }, - "@hono/hono@4.10.2": { - "integrity": "9cdb32df3b1f8eba43b197035a90e0c3c543ddf99f75cebfa31a2414bc5633f8" + "@hono/hono@4.11.1": { + "integrity": "28bb28c7322a1379e7132af05ef76aef09f86bb8cdd7077c67e004dea288ac0d" }, - "@logtape/file@1.2.2": { - "integrity": "a602f49148d0d5553dd3398506e9579e7294bab5706dff91f4c18abde53f1985", + "@logtape/file@1.3.5": { + "integrity": "6e5248e873e260109267b79bd3fb19f307a664a2233c5ae6f699d697549db985", "dependencies": [ - "jsr:@logtape/logtape@^1.2.2" + "jsr:@logtape/logtape@^1.3.5" ] }, - "@logtape/logtape@1.2.2": { - "integrity": "628fa8d52245aa1c81fd3af91622b621c1974fc8be71afb7574cd256b6aff953" + "@logtape/logtape@1.3.5": { + "integrity": "a5cdb130daf1a9d384006b0f850cc4443bfc2e163dadc6fa667875e79770beb3" }, - "@optique/core@0.6.1": { - "integrity": "87fe16d06724b8d83c114c7c734780426c34ff865627cda9ee21cd18ada198af" + "@optique/core@0.6.5": { + "integrity": "6568b8aef8b576e1b9ad8d57d5abdfe4dfeb960953205a1b9dced1426f7e0109" }, - "@optique/run@0.6.1": { - "integrity": "5e4017221e22dde1e731a81ccae7c0cf5d40e1f392b78193fe5ccc6475fb88b4", + "@optique/run@0.6.5": { + "integrity": "1c6ab2606ea4c5d2f6b851a857e95a37634797223e82aa8bc14af42988fc8fa9", "dependencies": [ - "jsr:@optique/core@~0.6.1" + "jsr:@optique/core@~0.6.5" ] }, "@std/bytes@1.0.6": { @@ -175,9 +182,6 @@ "dependencies": [ "jsr:@std/internal" ] - }, - "@std/yaml@1.0.10": { - "integrity": "245706ea3511cc50c8c6d00339c23ea2ffa27bd2c7ea5445338f8feff31fa58e" } }, "npm": { @@ -229,7 +233,7 @@ "mime@3.0.0" ] }, - "@cloudflare/unenv-preset@2.7.13_unenv@2.0.0-rc.24_workerd@1.20251210.0": { + "@cloudflare/unenv-preset@2.7.13_unenv@2.0.0-rc.24_workerd@1.20251217.0": { "integrity": "sha512-NulO1H8R/DzsJguLC0ndMuk4Ufv0KSlN+E54ay9rn9ZCQo0kpAPwwh3LhgpZ96a3Dr6L9LqW57M4CqC34iLOvw==", "dependencies": [ "unenv", @@ -239,33 +243,33 @@ "workerd" ] }, - "@cloudflare/workerd-darwin-64@1.20251210.0": { - "integrity": "sha512-Nn9X1moUDERA9xtFdCQ2XpQXgAS9pOjiCxvOT8sVx9UJLAiBLkfSCGbpsYdarODGybXCpjRlc77Yppuolvt7oQ==", + "@cloudflare/workerd-darwin-64@1.20251217.0": { + "integrity": "sha512-DN6vT+9ho61d/1/YuILW4VS+N1JBLaixWRL1vqNmhgbf8J8VHwWWotrRruEUYigJKx2yZyw6YsasE+yLXgx/Fw==", "os": ["darwin"], "cpu": ["x64"] }, - "@cloudflare/workerd-darwin-arm64@1.20251210.0": { - "integrity": "sha512-Mg8iYIZQFnbevq/ls9eW/eneWTk/EE13Pej1MwfkY5et0jVpdHnvOLywy/o+QtMJFef1AjsqXGULwAneYyBfHw==", + "@cloudflare/workerd-darwin-arm64@1.20251217.0": { + "integrity": "sha512-5nZOpRTkHmtcTc4Wbr1mj/O3dLb6aHZSiJuVBgtdbVcVmOXueSay3hnw1PXEyR+vpTKGUPkM+omUIslKHWnXDw==", "os": ["darwin"], "cpu": ["arm64"] }, - "@cloudflare/workerd-linux-64@1.20251210.0": { - "integrity": "sha512-kjC2fCZhZ2Gkm1biwk2qByAYpGguK5Gf5ic8owzSCUw0FOUfQxTZUT9Lp3gApxsfTLbbnLBrX/xzWjywH9QR4g==", + "@cloudflare/workerd-linux-64@1.20251217.0": { + "integrity": "sha512-uoPGhMaZVXPpCsU0oG3HQzyVpXCGi5rU+jcHRjUI7DXM4EwctBGvZ380Knkja36qtl+ZvSKVR1pUFSGdK+45Pg==", "os": ["linux"], "cpu": ["x64"] }, - "@cloudflare/workerd-linux-arm64@1.20251210.0": { - "integrity": "sha512-2IB37nXi7PZVQLa1OCuO7/6pNxqisRSO8DmCQ5x/3sezI5op1vwOxAcb1osAnuVsVN9bbvpw70HJvhKruFJTuA==", + "@cloudflare/workerd-linux-arm64@1.20251217.0": { + "integrity": "sha512-ixHnHKsiz1Xko+eDgCJOZ7EEUZKtmnYq3AjW3nkVcLFypSLks4C29E45zVewdaN4wq8sCLeyQCl6r1kS17+DQQ==", "os": ["linux"], "cpu": ["arm64"] }, - "@cloudflare/workerd-windows-64@1.20251210.0": { - "integrity": "sha512-Uaz6/9XE+D6E7pCY4OvkCuJHu7HcSDzeGcCGY1HLhojXhHd7yL52c3yfiyJdS8hPatiAa0nn5qSI/42+aTdDSw==", + "@cloudflare/workerd-windows-64@1.20251217.0": { + "integrity": "sha512-rP6USX+7ctynz3AtmKi+EvlLP3Xdr1ETrSdcnv693/I5QdUwBxq4yE1Lj6CV7GJizX6opXKYg8QMq0Q4eB9zRQ==", "os": ["win32"], "cpu": ["x64"] }, - "@cloudflare/workers-types@4.20251213.0": { - "integrity": "sha512-PJAGdKfU7hs39C2YOFNLTdrfdqG6rbaVj5UuI306zS+TPokiskRLEgUXKqS6avN9Uu9Nyuf2a0hqoumLQCnJlQ==" + "@cloudflare/workers-types@4.20251219.0": { + "integrity": "sha512-qwuvc3ZDdCfcK9dJrBSFHOsX8kL72sypfBilzEWbbb+slB2NiggjsHeGMV2ZQiQc1zyBMQPjIvsVeE7Apxp7hw==" }, "@colors/colors@1.5.0": { "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" @@ -302,400 +306,197 @@ "tslib" ] }, - "@esbuild/aix-ppc64@0.25.12": { - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "os": ["aix"], - "cpu": ["ppc64"] - }, "@esbuild/aix-ppc64@0.27.0": { "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", "os": ["aix"], "cpu": ["ppc64"] }, - "@esbuild/aix-ppc64@0.27.1": { - "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", - "os": ["aix"], - "cpu": ["ppc64"] - }, - "@esbuild/android-arm64@0.25.12": { - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "os": ["android"], - "cpu": ["arm64"] - }, "@esbuild/android-arm64@0.27.0": { "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", "os": ["android"], "cpu": ["arm64"] }, - "@esbuild/android-arm64@0.27.1": { - "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", - "os": ["android"], - "cpu": ["arm64"] - }, - "@esbuild/android-arm@0.25.12": { - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "os": ["android"], - "cpu": ["arm"] - }, "@esbuild/android-arm@0.27.0": { "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", "os": ["android"], "cpu": ["arm"] }, - "@esbuild/android-arm@0.27.1": { - "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", - "os": ["android"], - "cpu": ["arm"] - }, - "@esbuild/android-x64@0.25.12": { - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "os": ["android"], - "cpu": ["x64"] - }, "@esbuild/android-x64@0.27.0": { "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", "os": ["android"], "cpu": ["x64"] }, - "@esbuild/android-x64@0.27.1": { - "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", - "os": ["android"], - "cpu": ["x64"] - }, - "@esbuild/darwin-arm64@0.25.12": { - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "os": ["darwin"], - "cpu": ["arm64"] - }, "@esbuild/darwin-arm64@0.27.0": { "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", "os": ["darwin"], "cpu": ["arm64"] }, - "@esbuild/darwin-arm64@0.27.1": { - "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", - "os": ["darwin"], - "cpu": ["arm64"] - }, - "@esbuild/darwin-x64@0.25.12": { - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "os": ["darwin"], - "cpu": ["x64"] - }, "@esbuild/darwin-x64@0.27.0": { "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", "os": ["darwin"], "cpu": ["x64"] }, - "@esbuild/darwin-x64@0.27.1": { - "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", - "os": ["darwin"], - "cpu": ["x64"] - }, - "@esbuild/freebsd-arm64@0.25.12": { - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "os": ["freebsd"], - "cpu": ["arm64"] - }, "@esbuild/freebsd-arm64@0.27.0": { "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", "os": ["freebsd"], "cpu": ["arm64"] }, - "@esbuild/freebsd-arm64@0.27.1": { - "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", - "os": ["freebsd"], - "cpu": ["arm64"] - }, - "@esbuild/freebsd-x64@0.25.12": { - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "os": ["freebsd"], - "cpu": ["x64"] - }, "@esbuild/freebsd-x64@0.27.0": { "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", "os": ["freebsd"], "cpu": ["x64"] }, - "@esbuild/freebsd-x64@0.27.1": { - "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", - "os": ["freebsd"], - "cpu": ["x64"] - }, - "@esbuild/linux-arm64@0.25.12": { - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "os": ["linux"], - "cpu": ["arm64"] - }, "@esbuild/linux-arm64@0.27.0": { "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", "os": ["linux"], "cpu": ["arm64"] }, - "@esbuild/linux-arm64@0.27.1": { - "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", - "os": ["linux"], - "cpu": ["arm64"] - }, - "@esbuild/linux-arm@0.25.12": { - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "os": ["linux"], - "cpu": ["arm"] - }, "@esbuild/linux-arm@0.27.0": { "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", "os": ["linux"], "cpu": ["arm"] }, - "@esbuild/linux-arm@0.27.1": { - "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", - "os": ["linux"], - "cpu": ["arm"] - }, - "@esbuild/linux-ia32@0.25.12": { - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "os": ["linux"], - "cpu": ["ia32"] - }, "@esbuild/linux-ia32@0.27.0": { "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", "os": ["linux"], "cpu": ["ia32"] }, - "@esbuild/linux-ia32@0.27.1": { - "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", - "os": ["linux"], - "cpu": ["ia32"] - }, - "@esbuild/linux-loong64@0.25.12": { - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "os": ["linux"], - "cpu": ["loong64"] - }, "@esbuild/linux-loong64@0.27.0": { "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", "os": ["linux"], "cpu": ["loong64"] }, - "@esbuild/linux-loong64@0.27.1": { - "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", - "os": ["linux"], - "cpu": ["loong64"] - }, - "@esbuild/linux-mips64el@0.25.12": { - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "os": ["linux"], - "cpu": ["mips64el"] - }, "@esbuild/linux-mips64el@0.27.0": { "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", "os": ["linux"], "cpu": ["mips64el"] }, - "@esbuild/linux-mips64el@0.27.1": { - "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", - "os": ["linux"], - "cpu": ["mips64el"] - }, - "@esbuild/linux-ppc64@0.25.12": { - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "os": ["linux"], - "cpu": ["ppc64"] - }, "@esbuild/linux-ppc64@0.27.0": { "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", "os": ["linux"], "cpu": ["ppc64"] }, - "@esbuild/linux-ppc64@0.27.1": { - "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", - "os": ["linux"], - "cpu": ["ppc64"] - }, - "@esbuild/linux-riscv64@0.25.12": { - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "os": ["linux"], - "cpu": ["riscv64"] - }, "@esbuild/linux-riscv64@0.27.0": { "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", "os": ["linux"], "cpu": ["riscv64"] }, - "@esbuild/linux-riscv64@0.27.1": { - "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", - "os": ["linux"], - "cpu": ["riscv64"] - }, - "@esbuild/linux-s390x@0.25.12": { - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "os": ["linux"], - "cpu": ["s390x"] - }, "@esbuild/linux-s390x@0.27.0": { "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", "os": ["linux"], "cpu": ["s390x"] }, - "@esbuild/linux-s390x@0.27.1": { - "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", - "os": ["linux"], - "cpu": ["s390x"] - }, - "@esbuild/linux-x64@0.25.12": { - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "os": ["linux"], - "cpu": ["x64"] - }, "@esbuild/linux-x64@0.27.0": { "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", "os": ["linux"], "cpu": ["x64"] }, - "@esbuild/linux-x64@0.27.1": { - "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", - "os": ["linux"], - "cpu": ["x64"] - }, - "@esbuild/netbsd-arm64@0.25.12": { - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "os": ["netbsd"], - "cpu": ["arm64"] - }, "@esbuild/netbsd-arm64@0.27.0": { "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", "os": ["netbsd"], "cpu": ["arm64"] }, - "@esbuild/netbsd-arm64@0.27.1": { - "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", - "os": ["netbsd"], - "cpu": ["arm64"] - }, - "@esbuild/netbsd-x64@0.25.12": { - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "os": ["netbsd"], - "cpu": ["x64"] - }, "@esbuild/netbsd-x64@0.27.0": { "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", "os": ["netbsd"], "cpu": ["x64"] }, - "@esbuild/netbsd-x64@0.27.1": { - "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", - "os": ["netbsd"], - "cpu": ["x64"] - }, - "@esbuild/openbsd-arm64@0.25.12": { - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "os": ["openbsd"], - "cpu": ["arm64"] - }, "@esbuild/openbsd-arm64@0.27.0": { "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", "os": ["openbsd"], "cpu": ["arm64"] }, - "@esbuild/openbsd-arm64@0.27.1": { - "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", - "os": ["openbsd"], - "cpu": ["arm64"] - }, - "@esbuild/openbsd-x64@0.25.12": { - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "os": ["openbsd"], - "cpu": ["x64"] - }, "@esbuild/openbsd-x64@0.27.0": { "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", "os": ["openbsd"], "cpu": ["x64"] }, - "@esbuild/openbsd-x64@0.27.1": { - "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", - "os": ["openbsd"], - "cpu": ["x64"] - }, - "@esbuild/openharmony-arm64@0.25.12": { - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "os": ["openharmony"], - "cpu": ["arm64"] - }, "@esbuild/openharmony-arm64@0.27.0": { "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@esbuild/openharmony-arm64@0.27.1": { - "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", - "os": ["openharmony"], - "cpu": ["arm64"] - }, - "@esbuild/sunos-x64@0.25.12": { - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "os": ["sunos"], - "cpu": ["x64"] - }, "@esbuild/sunos-x64@0.27.0": { "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", "os": ["sunos"], "cpu": ["x64"] }, - "@esbuild/sunos-x64@0.27.1": { - "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", - "os": ["sunos"], - "cpu": ["x64"] - }, - "@esbuild/win32-arm64@0.25.12": { - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "os": ["win32"], - "cpu": ["arm64"] - }, "@esbuild/win32-arm64@0.27.0": { "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", "os": ["win32"], "cpu": ["arm64"] }, - "@esbuild/win32-arm64@0.27.1": { - "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", - "os": ["win32"], - "cpu": ["arm64"] - }, - "@esbuild/win32-ia32@0.25.12": { - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "os": ["win32"], - "cpu": ["ia32"] - }, "@esbuild/win32-ia32@0.27.0": { "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", "os": ["win32"], "cpu": ["ia32"] }, - "@esbuild/win32-ia32@0.27.1": { - "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", - "os": ["win32"], - "cpu": ["ia32"] - }, - "@esbuild/win32-x64@0.25.12": { - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "os": ["win32"], - "cpu": ["x64"] - }, "@esbuild/win32-x64@0.27.0": { "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", "os": ["win32"], "cpu": ["x64"] }, - "@esbuild/win32-x64@0.27.1": { - "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", - "os": ["win32"], - "cpu": ["x64"] + "@eslint-community/eslint-utils@4.9.0_eslint@9.39.2": { + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dependencies": [ + "eslint", + "eslint-visitor-keys@3.4.3" + ] + }, + "@eslint-community/regexpp@4.12.2": { + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==" + }, + "@eslint/config-array@0.21.1": { + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dependencies": [ + "@eslint/object-schema", + "debug@4.4.3", + "minimatch@3.1.2" + ] + }, + "@eslint/config-helpers@0.4.2": { + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dependencies": [ + "@eslint/core" + ] + }, + "@eslint/core@0.17.0": { + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dependencies": [ + "@types/json-schema" + ] + }, + "@eslint/eslintrc@3.3.3": { + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dependencies": [ + "ajv@6.12.6", + "debug@4.4.3", + "espree", + "globals", + "ignore", + "import-fresh", + "js-yaml", + "minimatch@3.1.2", + "strip-json-comments" + ] + }, + "@eslint/js@9.39.2": { + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==" + }, + "@eslint/object-schema@2.1.7": { + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==" + }, + "@eslint/plugin-kit@0.4.1": { + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dependencies": [ + "@eslint/core", + "levn" + ] }, "@fastify/ajv-compiler@4.0.5_ajv@8.17.1": { "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", "dependencies": [ - "ajv", + "ajv@8.17.1", "ajv-formats", "fast-uri" ] @@ -737,6 +538,22 @@ "@logtape/logtape" ] }, + "@humanfs/core@0.19.1": { + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==" + }, + "@humanfs/node@0.16.7": { + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dependencies": [ + "@humanfs/core", + "@humanwhocodes/retry" + ] + }, + "@humanwhocodes/module-importer@1.0.1": { + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" + }, + "@humanwhocodes/retry@0.4.3": { + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==" + }, "@img/sharp-darwin-arm64@0.33.5": { "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", "optionalDependencies": [ @@ -861,38 +678,38 @@ "@inquirer/ansi@1.0.2": { "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==" }, - "@inquirer/checkbox@4.3.2_@types+node@22.19.2": { + "@inquirer/checkbox@4.3.2_@types+node@22.19.3": { "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", "dependencies": [ "@inquirer/ansi", - "@inquirer/core@10.3.2_@types+node@22.19.2", + "@inquirer/core@10.3.2_@types+node@22.19.3", "@inquirer/figures", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3", "yoctocolors-cjs" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/confirm@5.1.21_@types+node@22.19.2": { + "@inquirer/confirm@5.1.21_@types+node@22.19.3": { "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", "dependencies": [ - "@inquirer/core@10.3.2_@types+node@22.19.2", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2" + "@inquirer/core@10.3.2_@types+node@22.19.3", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/core@10.3.2_@types+node@22.19.2": { + "@inquirer/core@10.3.2_@types+node@22.19.3": { "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", "dependencies": [ "@inquirer/ansi", "@inquirer/figures", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3", "cli-width", "mute-stream@2.0.0", "signal-exit", @@ -900,7 +717,7 @@ "yoctocolors-cjs" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, "@inquirer/core@8.2.4": { @@ -909,7 +726,7 @@ "@inquirer/figures", "@inquirer/type@1.5.5", "@types/mute-stream", - "@types/node@20.19.26", + "@types/node@20.19.27", "@types/wrap-ansi", "ansi-escapes", "cli-spinners", @@ -921,79 +738,79 @@ "wrap-ansi@6.2.0" ] }, - "@inquirer/editor@4.2.23_@types+node@22.19.2": { + "@inquirer/editor@4.2.23_@types+node@22.19.3": { "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", "dependencies": [ - "@inquirer/core@10.3.2_@types+node@22.19.2", + "@inquirer/core@10.3.2_@types+node@22.19.3", "@inquirer/external-editor", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2" + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/expand@4.0.23_@types+node@22.19.2": { + "@inquirer/expand@4.0.23_@types+node@22.19.3": { "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", "dependencies": [ - "@inquirer/core@10.3.2_@types+node@22.19.2", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2", + "@inquirer/core@10.3.2_@types+node@22.19.3", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3", "yoctocolors-cjs" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/external-editor@1.0.3_@types+node@22.19.2": { + "@inquirer/external-editor@1.0.3_@types+node@22.19.3": { "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", "dependencies": [ - "@types/node@22.19.2", + "@types/node@22.19.3", "chardet", "iconv-lite@0.7.1" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, "@inquirer/figures@1.0.15": { "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==" }, - "@inquirer/input@4.3.1_@types+node@22.19.2": { + "@inquirer/input@4.3.1_@types+node@22.19.3": { "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", "dependencies": [ - "@inquirer/core@10.3.2_@types+node@22.19.2", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2" + "@inquirer/core@10.3.2_@types+node@22.19.3", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/number@3.0.23_@types+node@22.19.2": { + "@inquirer/number@3.0.23_@types+node@22.19.3": { "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", "dependencies": [ - "@inquirer/core@10.3.2_@types+node@22.19.2", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2" + "@inquirer/core@10.3.2_@types+node@22.19.3", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/password@4.0.23_@types+node@22.19.2": { + "@inquirer/password@4.0.23_@types+node@22.19.3": { "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", "dependencies": [ "@inquirer/ansi", - "@inquirer/core@10.3.2_@types+node@22.19.2", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2" + "@inquirer/core@10.3.2_@types+node@22.19.3", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/prompts@7.10.1_@types+node@22.19.2": { + "@inquirer/prompts@7.10.1_@types+node@22.19.3": { "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", "dependencies": [ "@inquirer/checkbox", @@ -1006,49 +823,49 @@ "@inquirer/rawlist", "@inquirer/search", "@inquirer/select", - "@types/node@22.19.2" + "@types/node@22.19.3" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/rawlist@4.1.11_@types+node@22.19.2": { + "@inquirer/rawlist@4.1.11_@types+node@22.19.3": { "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", "dependencies": [ - "@inquirer/core@10.3.2_@types+node@22.19.2", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2", + "@inquirer/core@10.3.2_@types+node@22.19.3", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3", "yoctocolors-cjs" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/search@3.2.2_@types+node@22.19.2": { + "@inquirer/search@3.2.2_@types+node@22.19.3": { "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", "dependencies": [ - "@inquirer/core@10.3.2_@types+node@22.19.2", + "@inquirer/core@10.3.2_@types+node@22.19.3", "@inquirer/figures", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3", "yoctocolors-cjs" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/select@4.4.2_@types+node@22.19.2": { + "@inquirer/select@4.4.2_@types+node@22.19.3": { "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", "dependencies": [ "@inquirer/ansi", - "@inquirer/core@10.3.2_@types+node@22.19.2", + "@inquirer/core@10.3.2_@types+node@22.19.3", "@inquirer/figures", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3", "yoctocolors-cjs" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, "@inquirer/type@1.5.5": { @@ -1057,13 +874,13 @@ "mute-stream@1.0.0" ] }, - "@inquirer/type@3.0.10_@types+node@22.19.2": { + "@inquirer/type@3.0.10_@types+node@22.19.3": { "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", "dependencies": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, "@ioredis/commands@1.4.0": { @@ -1399,8 +1216,8 @@ "wasm-feature-detect" ] }, - "@logtape/logtape@1.2.2": { - "integrity": "sha512-yx4uj7NUXo5pOk2lljtAf7UN9oDBw9xfxSDC4jxdbfnGDGmq4iJhenWBbU9XV8ilm8AUUMYXq2nk+qvz8E21DA==" + "@logtape/logtape@1.3.5": { + "integrity": "sha512-G+MxWB7Tbv/2764519+Cp6rKXUdRbe/GiRwTvlm/Wv/sNsiquRnx9Hzr9eXaIpAYLT4PrBlkthjJ4gmqdSPrFg==" }, "@lukeed/csprng@1.1.0": { "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==" @@ -1470,8 +1287,8 @@ "@optique/core" ] }, - "@oxc-project/types@0.102.0": { - "integrity": "sha512-8Skrw405g+/UJPKWJ1twIk3BIH2nXdiVlVNtYT23AXVwpsd79es4K+KYt06Fbnkc5BaTvk/COT2JuCLYdwnCdA==" + "@oxc-project/types@0.103.0": { + "integrity": "sha512-bkiYX5kaXWwUessFRSoXFkGIQTmc6dLGdxuRTrC+h8PSnIdZyuXHHlLAeTmOue5Br/a0/a7dHH0Gca6eXn9MKg==" }, "@pinojs/redact@0.4.0": { "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==" @@ -1505,183 +1322,183 @@ "quansync" ] }, - "@rolldown/binding-android-arm64@1.0.0-beta.54": { - "integrity": "sha512-zZRx/ur3Fai3fxiEmVp48+6GCBR48PRWJR1X3TTMn9yiq2bBHlYPgBaQtDOYWXv5H3J5dXujeTyGnuoY+kdGCg==", + "@rolldown/binding-android-arm64@1.0.0-beta.55": { + "integrity": "sha512-5cPpHdO+zp+klznZnIHRO1bMHDq5hS9cqXodEKAaa/dQTPDjnE91OwAsy3o1gT2x4QaY8NzdBXAvutYdaw0WeA==", "os": ["android"], "cpu": ["arm64"] }, - "@rolldown/binding-darwin-arm64@1.0.0-beta.54": { - "integrity": "sha512-zMyFEJmbIs91x22HAA/eUvmZHgjX8tGsD3TJ+WC9aY4bCdl3w84H9vMZmChSHAF1dYvGNH4KQDI2IubeZaCYtg==", + "@rolldown/binding-darwin-arm64@1.0.0-beta.55": { + "integrity": "sha512-l0887CGU2SXZr0UJmeEcXSvtDCOhDTTYXuoWbhrEJ58YQhQk24EVhDhHMTyjJb1PBRniUgNc1G0T51eF8z+TWw==", "os": ["darwin"], "cpu": ["arm64"] }, - "@rolldown/binding-darwin-x64@1.0.0-beta.54": { - "integrity": "sha512-Ex7QttdaVnEpmE/zroUT5Qm10e2+Vjd9q0LX9eXm59SitxDODMpC8GI1Rct5RrLf4GLU4DzdXBj6DGzuR+6g6w==", + "@rolldown/binding-darwin-x64@1.0.0-beta.55": { + "integrity": "sha512-d7qP2AVYzN0tYIP4vJ7nmr26xvmlwdkLD/jWIc9Z9dqh5y0UGPigO3m5eHoHq9BNazmwdD9WzDHbQZyXFZjgtA==", "os": ["darwin"], "cpu": ["x64"] }, - "@rolldown/binding-freebsd-x64@1.0.0-beta.54": { - "integrity": "sha512-E1XO10ryM/Vxw3Q1wvs9s2mSpVBfbHtzkbJcdu26qh17ZmVwNWLiIoqEcbkXm028YwkReG4Gd2gCZ3NxgTQ28Q==", + "@rolldown/binding-freebsd-x64@1.0.0-beta.55": { + "integrity": "sha512-j311E4NOB0VMmXHoDDZhrWidUf7L/Sa6bu/+i2cskvHKU40zcUNPSYeD2YiO2MX+hhDFa5bJwhliYfs+bTrSZw==", "os": ["freebsd"], "cpu": ["x64"] }, - "@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.54": { - "integrity": "sha512-oS73Uks8jczQR9pg0Bj718vap/x71exyJ5yuxu4X5V4MhwRQnky7ANSPm6ARUfraxOqt49IBfcMeGnw2rTSqdA==", + "@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.55": { + "integrity": "sha512-lAsaYWhfNTW2A/9O7zCpb5eIJBrFeNEatOS/DDOZ5V/95NHy50g4b/5ViCqchfyFqRb7MKUR18/+xWkIcDkeIw==", "os": ["linux"], "cpu": ["arm"] }, - "@rolldown/binding-linux-arm64-gnu@1.0.0-beta.54": { - "integrity": "sha512-pY8N2X5C+/ZQcy0eRdfOzOP//OFngP1TaIqDjFwfBPws2UNavKS8SpxhPEgUaYIaT0keVBd/TB+eVy9z+CIOtw==", + "@rolldown/binding-linux-arm64-gnu@1.0.0-beta.55": { + "integrity": "sha512-2x6ffiVLZrQv7Xii9+JdtyT1U3bQhKj59K3eRnYlrXsKyjkjfmiDUVx2n+zSyijisUqD62fcegmx2oLLfeTkCA==", "os": ["linux"], "cpu": ["arm64"] }, - "@rolldown/binding-linux-arm64-musl@1.0.0-beta.54": { - "integrity": "sha512-cgTooAFm2MUmFriB7IYaWBNyqrGlRPKG+yaK2rGFl2rcdOcO24urY4p3eyB0ogqsRLvJbIxwjjYiWiIP7Eo1Cw==", + "@rolldown/binding-linux-arm64-musl@1.0.0-beta.55": { + "integrity": "sha512-QbNncvqAXziya5wleI+OJvmceEE15vE4yn4qfbI/hwT/+8ZcqxyfRZOOh62KjisXxp4D0h3JZspycXYejxAU3w==", "os": ["linux"], "cpu": ["arm64"] }, - "@rolldown/binding-linux-x64-gnu@1.0.0-beta.54": { - "integrity": "sha512-nGyLT1Qau0W+kEL44V2jhHmvfS3wyJW08E4WEu2E6NuIy+uChKN1X0aoxzFIDi2owDsYaZYez/98/f268EupIQ==", + "@rolldown/binding-linux-x64-gnu@1.0.0-beta.55": { + "integrity": "sha512-YZCTZZM+rujxwVc6A+QZaNMJXVtmabmFYLG2VGQTKaBfYGvBKUgtbMEttnp/oZ88BMi2DzadBVhOmfQV8SuHhw==", "os": ["linux"], "cpu": ["x64"] }, - "@rolldown/binding-linux-x64-musl@1.0.0-beta.54": { - "integrity": "sha512-KH374P0TUjDXssROT/orvzaWrzGOptD13PTrltgKwbDprJTMknoLiYsOD6Ttz92O2VuAcCtFuJ1xbyFM2Uo/Xg==", + "@rolldown/binding-linux-x64-musl@1.0.0-beta.55": { + "integrity": "sha512-28q9OQ/DDpFh2keS4BVAlc3N65/wiqKbk5K1pgLdu/uWbKa8hgUJofhXxqO+a+Ya2HVTUuYHneWsI2u+eu3N5Q==", "os": ["linux"], "cpu": ["x64"] }, - "@rolldown/binding-openharmony-arm64@1.0.0-beta.54": { - "integrity": "sha512-oMAVO4wbfAbhpBxPsSp8R7ntL2DchpNfO+tGhN8/sI9jsbYwOv78uIW1fTwOBslhjTVFltGJ+l23mubNQcYNaQ==", + "@rolldown/binding-openharmony-arm64@1.0.0-beta.55": { + "integrity": "sha512-LiCA4BjCnm49B+j1lFzUtlC+4ZphBv0d0g5VqrEJua/uyv9Ey1v9tiaMql1C8c0TVSNDUmrkfHQ71vuQC7YfpQ==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@rolldown/binding-wasm32-wasi@1.0.0-beta.54": { - "integrity": "sha512-MYY/FmY+HehHiQkNx04W5oLy/Fqd1hXYqZmmorSDXvAHnxMbSgmdFicKsSYOg/sVGHBMEP1tTn6kV5sWrS45rA==", + "@rolldown/binding-wasm32-wasi@1.0.0-beta.55": { + "integrity": "sha512-nZ76tY7T0Oe8vamz5Cv5CBJvrqeQxwj1WaJ2GxX8Msqs0zsQMMcvoyxOf0glnJlxxgKjtoBxAOxaAU8ERbW6Tg==", "dependencies": [ "@napi-rs/wasm-runtime" ], "cpu": ["wasm32"] }, - "@rolldown/binding-win32-arm64-msvc@1.0.0-beta.54": { - "integrity": "sha512-66o3uKxUmcYskT9exskxs3OVduXf5x0ndlMkYOjSpBgqzhLtkub136yDvZkNT1OkNDET0odSwcU7aWdpnwzAyg==", + "@rolldown/binding-win32-arm64-msvc@1.0.0-beta.55": { + "integrity": "sha512-TFVVfLfhL1G+pWspYAgPK/FSqjiBtRKYX9hixfs508QVEZPQlubYAepHPA7kEa6lZXYj5ntzF87KC6RNhxo+ew==", "os": ["win32"], "cpu": ["arm64"] }, - "@rolldown/binding-win32-x64-msvc@1.0.0-beta.54": { - "integrity": "sha512-FbbbrboChLBXfeEsOfaypBGqzbdJ/CcSA2BPLCggojnIHy58Jo+AXV7HATY8opZk7194rRbokIT8AfPJtZAWtg==", + "@rolldown/binding-win32-x64-msvc@1.0.0-beta.55": { + "integrity": "sha512-j1WBlk0p+ISgLzMIgl0xHp1aBGXenoK2+qWYc/wil2Vse7kVOdFq9aeQ8ahK6/oxX2teQ5+eDvgjdywqTL+daA==", "os": ["win32"], "cpu": ["x64"] }, - "@rolldown/pluginutils@1.0.0-beta.54": { - "integrity": "sha512-AHgcZ+w7RIRZ65ihSQL8YuoKcpD9Scew4sEeP1BBUT9QdTo6KjwHrZZXjID6nL10fhKessCH6OPany2QKwAwTQ==" + "@rolldown/pluginutils@1.0.0-beta.55": { + "integrity": "sha512-vajw/B3qoi7aYnnD4BQ4VoCcXQWnF0roSwE2iynbNxgW4l9mFwtLmLmUhpDdcTBfKyZm1p/T0D13qG94XBLohA==" }, - "@rollup/rollup-android-arm-eabi@4.53.3": { - "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "@rollup/rollup-android-arm-eabi@4.53.5": { + "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", "os": ["android"], "cpu": ["arm"] }, - "@rollup/rollup-android-arm64@4.53.3": { - "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "@rollup/rollup-android-arm64@4.53.5": { + "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", "os": ["android"], "cpu": ["arm64"] }, - "@rollup/rollup-darwin-arm64@4.53.3": { - "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "@rollup/rollup-darwin-arm64@4.53.5": { + "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", "os": ["darwin"], "cpu": ["arm64"] }, - "@rollup/rollup-darwin-x64@4.53.3": { - "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "@rollup/rollup-darwin-x64@4.53.5": { + "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", "os": ["darwin"], "cpu": ["x64"] }, - "@rollup/rollup-freebsd-arm64@4.53.3": { - "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "@rollup/rollup-freebsd-arm64@4.53.5": { + "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", "os": ["freebsd"], "cpu": ["arm64"] }, - "@rollup/rollup-freebsd-x64@4.53.3": { - "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "@rollup/rollup-freebsd-x64@4.53.5": { + "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", "os": ["freebsd"], "cpu": ["x64"] }, - "@rollup/rollup-linux-arm-gnueabihf@4.53.3": { - "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "@rollup/rollup-linux-arm-gnueabihf@4.53.5": { + "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", "os": ["linux"], "cpu": ["arm"] }, - "@rollup/rollup-linux-arm-musleabihf@4.53.3": { - "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "@rollup/rollup-linux-arm-musleabihf@4.53.5": { + "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", "os": ["linux"], "cpu": ["arm"] }, - "@rollup/rollup-linux-arm64-gnu@4.53.3": { - "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "@rollup/rollup-linux-arm64-gnu@4.53.5": { + "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", "os": ["linux"], "cpu": ["arm64"] }, - "@rollup/rollup-linux-arm64-musl@4.53.3": { - "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "@rollup/rollup-linux-arm64-musl@4.53.5": { + "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", "os": ["linux"], "cpu": ["arm64"] }, - "@rollup/rollup-linux-loong64-gnu@4.53.3": { - "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "@rollup/rollup-linux-loong64-gnu@4.53.5": { + "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", "os": ["linux"], "cpu": ["loong64"] }, - "@rollup/rollup-linux-ppc64-gnu@4.53.3": { - "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "@rollup/rollup-linux-ppc64-gnu@4.53.5": { + "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", "os": ["linux"], "cpu": ["ppc64"] }, - "@rollup/rollup-linux-riscv64-gnu@4.53.3": { - "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "@rollup/rollup-linux-riscv64-gnu@4.53.5": { + "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", "os": ["linux"], "cpu": ["riscv64"] }, - "@rollup/rollup-linux-riscv64-musl@4.53.3": { - "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "@rollup/rollup-linux-riscv64-musl@4.53.5": { + "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", "os": ["linux"], "cpu": ["riscv64"] }, - "@rollup/rollup-linux-s390x-gnu@4.53.3": { - "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "@rollup/rollup-linux-s390x-gnu@4.53.5": { + "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", "os": ["linux"], "cpu": ["s390x"] }, - "@rollup/rollup-linux-x64-gnu@4.53.3": { - "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "@rollup/rollup-linux-x64-gnu@4.53.5": { + "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", "os": ["linux"], "cpu": ["x64"] }, - "@rollup/rollup-linux-x64-musl@4.53.3": { - "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "@rollup/rollup-linux-x64-musl@4.53.5": { + "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", "os": ["linux"], "cpu": ["x64"] }, - "@rollup/rollup-openharmony-arm64@4.53.3": { - "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "@rollup/rollup-openharmony-arm64@4.53.5": { + "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@rollup/rollup-win32-arm64-msvc@4.53.3": { - "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "@rollup/rollup-win32-arm64-msvc@4.53.5": { + "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", "os": ["win32"], "cpu": ["arm64"] }, - "@rollup/rollup-win32-ia32-msvc@4.53.3": { - "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "@rollup/rollup-win32-ia32-msvc@4.53.5": { + "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", "os": ["win32"], "cpu": ["ia32"] }, - "@rollup/rollup-win32-x64-gnu@4.53.3": { - "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "@rollup/rollup-win32-x64-gnu@4.53.5": { + "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==", "os": ["win32"], "cpu": ["x64"] }, - "@rollup/rollup-win32-x64-msvc@4.53.3": { - "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "@rollup/rollup-win32-x64-msvc@4.53.5": { + "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==", "os": ["win32"], "cpu": ["x64"] }, @@ -1742,8 +1559,8 @@ "@speed-highlight/core@1.2.12": { "integrity": "sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==" }, - "@standard-schema/spec@1.0.0": { - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==" + "@standard-schema/spec@1.1.0": { + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==" }, "@sveltejs/acorn-typescript@1.0.8_acorn@8.15.0": { "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==", @@ -1751,7 +1568,7 @@ "acorn@8.15.0" ] }, - "@sveltejs/kit@2.49.2_@opentelemetry+api@1.9.0_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.46.0___acorn@8.15.0__vite@7.2.7___@types+node@22.19.2___tsx@4.21.0___yaml@2.8.2___picomatch@4.0.3__@types+node@22.19.2__tsx@4.21.0__yaml@2.8.2_svelte@5.46.0__acorn@8.15.0_vite@7.2.7__@types+node@22.19.2__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_acorn@8.15.0_@types+node@22.19.2_tsx@4.21.0_yaml@2.8.2": { + "@sveltejs/kit@2.49.2_@opentelemetry+api@1.9.0_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.46.0___acorn@8.15.0__vite@7.3.0___@types+node@22.19.3___tsx@4.21.0___yaml@2.8.2___picomatch@4.0.3__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2_svelte@5.46.0__acorn@8.15.0_vite@7.3.0__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_acorn@8.15.0_@types+node@22.19.3_tsx@4.21.0_yaml@2.8.2": { "integrity": "sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==", "dependencies": [ "@opentelemetry/api", @@ -1777,7 +1594,7 @@ ], "bin": true }, - "@sveltejs/vite-plugin-svelte-inspector@5.0.1_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.46.0___acorn@8.15.0__vite@7.2.7___@types+node@22.19.2___tsx@4.21.0___yaml@2.8.2___picomatch@4.0.3__@types+node@22.19.2__tsx@4.21.0__yaml@2.8.2_svelte@5.46.0__acorn@8.15.0_vite@7.2.7__@types+node@22.19.2__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_@types+node@22.19.2_tsx@4.21.0_yaml@2.8.2": { + "@sveltejs/vite-plugin-svelte-inspector@5.0.1_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.46.0___acorn@8.15.0__vite@7.3.0___@types+node@22.19.3___tsx@4.21.0___yaml@2.8.2___picomatch@4.0.3__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2_svelte@5.46.0__acorn@8.15.0_vite@7.3.0__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_@types+node@22.19.3_tsx@4.21.0_yaml@2.8.2": { "integrity": "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==", "dependencies": [ "@sveltejs/vite-plugin-svelte", @@ -1786,7 +1603,7 @@ "vite" ] }, - "@sveltejs/vite-plugin-svelte@6.2.1_svelte@5.46.0__acorn@8.15.0_vite@7.2.7__@types+node@22.19.2__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_@types+node@22.19.2_tsx@4.21.0_yaml@2.8.2": { + "@sveltejs/vite-plugin-svelte@6.2.1_svelte@5.46.0__acorn@8.15.0_vite@7.3.0__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_@types+node@22.19.3_tsx@4.21.0_yaml@2.8.2": { "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "dependencies": [ "@sveltejs/vite-plugin-svelte-inspector", @@ -1818,12 +1635,19 @@ "@types/amqplib@0.10.8": { "integrity": "sha512-vtDp8Pk1wsE/AuQ8/Rgtm6KUZYqcnTgNvEHwzCkX8rL7AGsC6zqAfKAAJhUZXFhM/Pp++tbnUHiam/8vVpPztA==", "dependencies": [ - "@types/node@24.10.3" + "@types/node@24.2.0" ] }, "@types/cookie@0.6.0": { "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" }, + "@types/eslint@9.6.1": { + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dependencies": [ + "@types/estree", + "@types/json-schema" + ] + }, "@types/estree@1.0.8": { "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, @@ -1836,6 +1660,9 @@ "@types/unist" ] }, + "@types/json-schema@7.0.15": { + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, "@types/mdast@4.0.4": { "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "dependencies": [ @@ -1845,36 +1672,112 @@ "@types/mute-stream@0.0.4": { "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", "dependencies": [ - "@types/node@24.10.3" + "@types/node@24.2.0" ] }, "@types/node@16.9.1": { "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==" }, - "@types/node@20.19.26": { - "integrity": "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==", + "@types/node@20.19.27": { + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "dependencies": [ "undici-types@6.21.0" ] }, - "@types/node@22.19.2": { - "integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==", + "@types/node@22.19.3": { + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "dependencies": [ "undici-types@6.21.0" ] }, - "@types/node@24.10.3": { - "integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==", + "@types/node@24.10.4": { + "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", "dependencies": [ "undici-types@7.16.0" ] }, + "@types/node@24.2.0": { + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", + "dependencies": [ + "undici-types@7.10.0" + ] + }, "@types/unist@3.0.3": { "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, "@types/wrap-ansi@3.0.0": { "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==" }, + "@typescript-eslint/parser@8.50.0_eslint@9.39.2_typescript@5.9.3": { + "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", + "dependencies": [ + "@typescript-eslint/scope-manager", + "@typescript-eslint/types", + "@typescript-eslint/typescript-estree", + "@typescript-eslint/visitor-keys", + "debug@4.4.3", + "eslint", + "typescript" + ] + }, + "@typescript-eslint/project-service@8.50.0_typescript@5.9.3": { + "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==", + "dependencies": [ + "@typescript-eslint/tsconfig-utils", + "@typescript-eslint/types", + "debug@4.4.3", + "typescript" + ] + }, + "@typescript-eslint/scope-manager@8.50.0": { + "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", + "dependencies": [ + "@typescript-eslint/types", + "@typescript-eslint/visitor-keys" + ] + }, + "@typescript-eslint/tsconfig-utils@8.50.0_typescript@5.9.3": { + "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==", + "dependencies": [ + "typescript" + ] + }, + "@typescript-eslint/types@8.50.0": { + "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==" + }, + "@typescript-eslint/typescript-estree@8.50.0_typescript@5.9.3": { + "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", + "dependencies": [ + "@typescript-eslint/project-service", + "@typescript-eslint/tsconfig-utils", + "@typescript-eslint/types", + "@typescript-eslint/visitor-keys", + "debug@4.4.3", + "minimatch@9.0.5", + "semver", + "tinyglobby", + "ts-api-utils", + "typescript" + ] + }, + "@typescript-eslint/utils@8.50.0_eslint@9.39.2_typescript@5.9.3": { + "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", + "dependencies": [ + "@eslint-community/eslint-utils", + "@typescript-eslint/scope-manager", + "@typescript-eslint/types", + "@typescript-eslint/typescript-estree", + "eslint", + "typescript" + ] + }, + "@typescript-eslint/visitor-keys@8.50.0": { + "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", + "dependencies": [ + "@typescript-eslint/types", + "eslint-visitor-keys@4.2.1" + ] + }, "@ungap/structured-clone@1.3.0": { "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" }, @@ -1894,6 +1797,12 @@ "negotiator" ] }, + "acorn-jsx@5.3.2_acorn@8.15.0": { + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dependencies": [ + "acorn@8.15.0" + ] + }, "acorn-walk@8.3.2": { "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==" }, @@ -1908,10 +1817,19 @@ "ajv-formats@3.0.1_ajv@8.17.1": { "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dependencies": [ - "ajv" + "ajv@8.17.1" ], "optionalPeers": [ - "ajv" + "ajv@8.17.1" + ] + }, + "ajv@6.12.6": { + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": [ + "fast-deep-equal", + "fast-json-stable-stringify", + "json-schema-traverse@0.4.1", + "uri-js" ] }, "ajv@8.17.1": { @@ -1919,7 +1837,7 @@ "dependencies": [ "fast-deep-equal", "fast-uri", - "json-schema-traverse", + "json-schema-traverse@1.0.0", "require-from-string" ] }, @@ -1960,6 +1878,9 @@ "any-promise@1.3.0": { "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" }, + "argparse@2.0.1": { + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, "aria-query@5.3.2": { "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==" }, @@ -1997,6 +1918,9 @@ "axobject-query@4.1.0": { "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==" }, + "balanced-match@1.0.2": { + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, "base64-js@1.5.1": { "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, @@ -2029,6 +1953,19 @@ "unpipe" ] }, + "brace-expansion@1.1.12": { + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dependencies": [ + "balanced-match", + "concat-map" + ] + }, + "brace-expansion@2.0.2": { + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dependencies": [ + "balanced-match" + ] + }, "buffer-more-ints@1.0.0": { "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==" }, @@ -2079,6 +2016,9 @@ "get-intrinsic" ] }, + "callsites@3.1.0": { + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, "canonicalize@2.1.0": { "integrity": "sha512-F705O3xrsUtgt98j7leetNhTWPe+5S72rlL5O4jA1pKqBVQ/dT1O1D6PFxmSXvc0SUOinWS57DKx0I3CHrXJHQ==", "bin": true @@ -2187,6 +2127,9 @@ "comma-separated-tokens@2.0.3": { "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==" }, + "concat-map@0.0.1": { + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, "content-disposition@0.5.4": { "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dependencies": [ @@ -2218,6 +2161,14 @@ "keygrip" ] }, + "cross-spawn@7.0.6": { + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": [ + "path-key", + "shebang-command", + "which" + ] + }, "crossws@0.3.5": { "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", "dependencies": [ @@ -2254,6 +2205,9 @@ "deep-equal@1.0.1": { "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==" }, + "deep-is@0.1.4": { + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, "deepmerge@4.3.1": { "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" }, @@ -2356,101 +2310,35 @@ "es-toolkit@1.43.0": { "integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==" }, - "esbuild@0.25.12": { - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "optionalDependencies": [ - "@esbuild/aix-ppc64@0.25.12", - "@esbuild/android-arm@0.25.12", - "@esbuild/android-arm64@0.25.12", - "@esbuild/android-x64@0.25.12", - "@esbuild/darwin-arm64@0.25.12", - "@esbuild/darwin-x64@0.25.12", - "@esbuild/freebsd-arm64@0.25.12", - "@esbuild/freebsd-x64@0.25.12", - "@esbuild/linux-arm@0.25.12", - "@esbuild/linux-arm64@0.25.12", - "@esbuild/linux-ia32@0.25.12", - "@esbuild/linux-loong64@0.25.12", - "@esbuild/linux-mips64el@0.25.12", - "@esbuild/linux-ppc64@0.25.12", - "@esbuild/linux-riscv64@0.25.12", - "@esbuild/linux-s390x@0.25.12", - "@esbuild/linux-x64@0.25.12", - "@esbuild/netbsd-arm64@0.25.12", - "@esbuild/netbsd-x64@0.25.12", - "@esbuild/openbsd-arm64@0.25.12", - "@esbuild/openbsd-x64@0.25.12", - "@esbuild/openharmony-arm64@0.25.12", - "@esbuild/sunos-x64@0.25.12", - "@esbuild/win32-arm64@0.25.12", - "@esbuild/win32-ia32@0.25.12", - "@esbuild/win32-x64@0.25.12" - ], - "scripts": true, - "bin": true - }, "esbuild@0.27.0": { "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", "optionalDependencies": [ - "@esbuild/aix-ppc64@0.27.0", - "@esbuild/android-arm@0.27.0", - "@esbuild/android-arm64@0.27.0", - "@esbuild/android-x64@0.27.0", - "@esbuild/darwin-arm64@0.27.0", - "@esbuild/darwin-x64@0.27.0", - "@esbuild/freebsd-arm64@0.27.0", - "@esbuild/freebsd-x64@0.27.0", - "@esbuild/linux-arm@0.27.0", - "@esbuild/linux-arm64@0.27.0", - "@esbuild/linux-ia32@0.27.0", - "@esbuild/linux-loong64@0.27.0", - "@esbuild/linux-mips64el@0.27.0", - "@esbuild/linux-ppc64@0.27.0", - "@esbuild/linux-riscv64@0.27.0", - "@esbuild/linux-s390x@0.27.0", - "@esbuild/linux-x64@0.27.0", - "@esbuild/netbsd-arm64@0.27.0", - "@esbuild/netbsd-x64@0.27.0", - "@esbuild/openbsd-arm64@0.27.0", - "@esbuild/openbsd-x64@0.27.0", - "@esbuild/openharmony-arm64@0.27.0", - "@esbuild/sunos-x64@0.27.0", - "@esbuild/win32-arm64@0.27.0", - "@esbuild/win32-ia32@0.27.0", - "@esbuild/win32-x64@0.27.0" - ], - "scripts": true, - "bin": true - }, - "esbuild@0.27.1": { - "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", - "optionalDependencies": [ - "@esbuild/aix-ppc64@0.27.1", - "@esbuild/android-arm@0.27.1", - "@esbuild/android-arm64@0.27.1", - "@esbuild/android-x64@0.27.1", - "@esbuild/darwin-arm64@0.27.1", - "@esbuild/darwin-x64@0.27.1", - "@esbuild/freebsd-arm64@0.27.1", - "@esbuild/freebsd-x64@0.27.1", - "@esbuild/linux-arm@0.27.1", - "@esbuild/linux-arm64@0.27.1", - "@esbuild/linux-ia32@0.27.1", - "@esbuild/linux-loong64@0.27.1", - "@esbuild/linux-mips64el@0.27.1", - "@esbuild/linux-ppc64@0.27.1", - "@esbuild/linux-riscv64@0.27.1", - "@esbuild/linux-s390x@0.27.1", - "@esbuild/linux-x64@0.27.1", - "@esbuild/netbsd-arm64@0.27.1", - "@esbuild/netbsd-x64@0.27.1", - "@esbuild/openbsd-arm64@0.27.1", - "@esbuild/openbsd-x64@0.27.1", - "@esbuild/openharmony-arm64@0.27.1", - "@esbuild/sunos-x64@0.27.1", - "@esbuild/win32-arm64@0.27.1", - "@esbuild/win32-ia32@0.27.1", - "@esbuild/win32-x64@0.27.1" + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-arm64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-arm64", + "@esbuild/openbsd-x64", + "@esbuild/openharmony-arm64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" ], "scripts": true, "bin": true @@ -2461,15 +2349,97 @@ "escape-html@1.0.3": { "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "escape-string-regexp@4.0.0": { + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "eslint-scope@8.4.0": { + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dependencies": [ + "esrecurse", + "estraverse" + ] + }, + "eslint-visitor-keys@3.4.3": { + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" + }, + "eslint-visitor-keys@4.2.1": { + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==" + }, + "eslint@9.39.2": { + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dependencies": [ + "@eslint-community/eslint-utils", + "@eslint-community/regexpp", + "@eslint/config-array", + "@eslint/config-helpers", + "@eslint/core", + "@eslint/eslintrc", + "@eslint/js", + "@eslint/plugin-kit", + "@humanfs/node", + "@humanwhocodes/module-importer", + "@humanwhocodes/retry", + "@types/estree", + "ajv@6.12.6", + "chalk@4.1.2", + "cross-spawn", + "debug@4.4.3", + "escape-string-regexp", + "eslint-scope", + "eslint-visitor-keys@4.2.1", + "espree", + "esquery", + "esutils", + "fast-deep-equal", + "file-entry-cache", + "find-up", + "glob-parent", + "ignore", + "imurmurhash", + "is-glob", + "json-stable-stringify-without-jsonify", + "lodash.merge", + "minimatch@3.1.2", + "natural-compare", + "optionator" + ], + "bin": true + }, "esm-env@1.2.2": { "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" }, + "espree@10.4.0_acorn@8.15.0": { + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dependencies": [ + "acorn@8.15.0", + "acorn-jsx", + "eslint-visitor-keys@4.2.1" + ] + }, + "esquery@1.6.0": { + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dependencies": [ + "estraverse" + ] + }, "esrap@2.2.1": { "integrity": "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==", "dependencies": [ "@jridgewell/sourcemap-codec" ] }, + "esrecurse@4.3.0": { + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": [ + "estraverse" + ] + }, + "estraverse@5.3.0": { + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "esutils@2.0.3": { + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, "etag@1.8.1": { "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, @@ -2533,17 +2503,23 @@ "fast-deep-equal@3.1.3": { "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "fast-json-stable-stringify@2.1.0": { + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, "fast-json-stringify@6.1.1_ajv@8.17.1": { "integrity": "sha512-DbgptncYEXZqDUOEl4krff4mUiVrTZZVI7BBrQR/T3BqMj/eM1flTC1Uk2uUoLcWCxjT95xKulV/Lc6hhOZsBQ==", "dependencies": [ "@fastify/merge-json-schemas", - "ajv", + "ajv@8.17.1", "ajv-formats", "fast-uri", "json-schema-ref-resolver", "rfdc" ] }, + "fast-levenshtein@2.0.6": { + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, "fast-querystring@1.1.2": { "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", "dependencies": [ @@ -2603,6 +2579,12 @@ "fflate@0.8.2": { "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" }, + "file-entry-cache@8.0.0": { + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dependencies": [ + "flat-cache" + ] + }, "file-type@16.5.4": { "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", "dependencies": [ @@ -2649,6 +2631,23 @@ "safe-regex2" ] }, + "find-up@5.0.0": { + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": [ + "locate-path", + "path-exists" + ] + }, + "flat-cache@4.0.1": { + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dependencies": [ + "flatted", + "keyv" + ] + }, + "flatted@3.3.3": { + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" + }, "forwarded@0.2.0": { "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" }, @@ -2714,9 +2713,18 @@ "omggif" ] }, + "glob-parent@6.0.2": { + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": [ + "is-glob" + ] + }, "glob-to-regexp@0.4.1": { "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" }, + "globals@14.0.0": { + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==" + }, "gopd@1.2.0": { "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, @@ -2777,8 +2785,8 @@ "highlight.js@10.7.3": { "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" }, - "hono@4.11.0": { - "integrity": "sha512-Jg8uZzN2ul8/qlyid5FO8O624F3AK0wKtkgoeEON1qBum1rM1itYBxoMKu/1SPJC7F1+xlIZsJMmc4HHhJ1AWg==" + "hono@4.11.1": { + "integrity": "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg==" }, "hookable@5.5.3": { "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==" @@ -2803,16 +2811,6 @@ "toidentifier" ] }, - "http-errors@2.0.0": { - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": [ - "depd@2.0.0", - "inherits", - "setprototypeof", - "statuses@2.0.1", - "toidentifier" - ] - }, "http-errors@2.0.1": { "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dependencies": [ @@ -2849,12 +2847,25 @@ "ieee754@1.2.1": { "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, + "ignore@5.3.2": { + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" + }, "image-q@4.0.0": { "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", "dependencies": [ "@types/node@16.9.1" ] }, + "import-fresh@3.3.1": { + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dependencies": [ + "parent-module", + "resolve-from" + ] + }, + "imurmurhash@0.1.4": { + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + }, "inherits@2.0.4": { "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, @@ -2864,20 +2875,20 @@ "@inquirer/core@8.2.4" ] }, - "inquirer@12.11.1_@types+node@22.19.2": { + "inquirer@12.11.1_@types+node@22.19.3": { "integrity": "sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==", "dependencies": [ "@inquirer/ansi", - "@inquirer/core@10.3.2_@types+node@22.19.2", + "@inquirer/core@10.3.2_@types+node@22.19.3", "@inquirer/prompts", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3", "mute-stream@2.0.0", "run-async", "rxjs" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, "ioredis@5.8.2": { @@ -2906,6 +2917,9 @@ "is-arrayish@0.3.4": { "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==" }, + "is-extglob@2.1.1": { + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, "is-fullwidth-code-point@3.0.0": { "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, @@ -2919,6 +2933,12 @@ "safe-regex-test" ] }, + "is-glob@4.0.3": { + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": [ + "is-extglob" + ] + }, "is-interactive@2.0.0": { "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==" }, @@ -2946,6 +2966,9 @@ "is-unicode-supported@2.1.0": { "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==" }, + "isexe@2.0.0": { + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, "isomorphic-fetch@3.0.0": { "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", "dependencies": [ @@ -2995,6 +3018,13 @@ "jpeg-js@0.4.4": { "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==" }, + "js-yaml@4.1.1": { + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dependencies": [ + "argparse" + ], + "bin": true + }, "jsbi@4.3.2": { "integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==" }, @@ -3002,6 +3032,9 @@ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "bin": true }, + "json-buffer@3.0.1": { + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, "json-canon@1.0.1": { "integrity": "sha512-PQcj4PFOTAQxE8PgoQ4KrM0DcKWZd7S3ELOON8rmysl9I8JuFMgxu1H9v+oZsTPjjkpeS3IHPwLjr7d+gKygnw==" }, @@ -3017,9 +3050,15 @@ "dequal" ] }, + "json-schema-traverse@0.4.1": { + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "json-schema-traverse@1.0.0": { "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, + "json-stable-stringify-without-jsonify@1.0.1": { + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, "jsonld@9.0.0": { "integrity": "sha512-pjMIdkXfC1T2wrX9B9i2uXhGdyCmgec3qgMht+TDj+S0qX3bjWMQUfL7NeqEhuRTi8G5ESzmL9uGlST7nzSEWg==", "dependencies": [ @@ -3036,6 +3075,12 @@ ], "deprecated": true }, + "keyv@4.5.4": { + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": [ + "json-buffer" + ] + }, "kleur@4.1.5": { "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" }, @@ -3080,6 +3125,13 @@ "ky@1.14.1": { "integrity": "sha512-hYje4L9JCmpEQBtudo+v52X5X8tgWXUYyPcxKSuxQNboqufecl9VMWjGiucAFH060AwPXHZuH+WB2rrqfkmafw==" }, + "levn@0.4.1": { + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": [ + "prelude-ls", + "type-check" + ] + }, "light-my-request@6.6.0": { "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", "dependencies": [ @@ -3094,12 +3146,21 @@ "locate-character@3.0.0": { "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" }, + "locate-path@6.0.0": { + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": [ + "p-locate" + ] + }, "lodash.defaults@4.2.0": { "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, "lodash.isarguments@3.1.0": { "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" }, + "lodash.merge@4.6.2": { + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, "lodash@4.17.21": { "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, @@ -3192,8 +3253,8 @@ "mimic-function@5.0.1": { "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==" }, - "miniflare@4.20251210.0": { - "integrity": "sha512-k6kIoXwGVqlPZb0hcn+X7BmnK+8BjIIkusQPY22kCo2RaQJ/LzAjtxHQdGXerlHSnJyQivDQsL6BJHMpQfUFyw==", + "miniflare@4.20251217.0": { + "integrity": "sha512-8xsTQbPS6YV+ABZl9qiJIbsum6hbpbhqiyKpOVdzZrhK+1N8EFpT8R6aBZff7kezGmxYZSntjgjqTwJmj3JLgA==", "dependencies": [ "@cspotcode/source-map-support", "acorn@8.14.0", @@ -3210,6 +3271,18 @@ ], "bin": true }, + "minimatch@3.1.2": { + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": [ + "brace-expansion@1.1.12" + ] + }, + "minimatch@9.0.5": { + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": [ + "brace-expansion@2.0.2" + ] + }, "mri@1.2.0": { "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" }, @@ -3251,6 +3324,9 @@ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "bin": true }, + "natural-compare@1.4.0": { + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, "negotiator@0.6.3": { "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, @@ -3298,6 +3374,17 @@ "only@0.0.2": { "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==" }, + "optionator@0.9.4": { + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dependencies": [ + "deep-is", + "fast-levenshtein", + "levn", + "prelude-ls", + "type-check", + "word-wrap" + ] + }, "ora@8.2.0": { "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", "dependencies": [ @@ -3312,9 +3399,27 @@ "strip-ansi@7.1.2" ] }, + "p-limit@3.1.0": { + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": [ + "yocto-queue" + ] + }, + "p-locate@5.0.0": { + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": [ + "p-limit" + ] + }, "pako@1.0.11": { "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, + "parent-module@1.0.1": { + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": [ + "callsites" + ] + }, "parse-bmfont-ascii@1.0.6": { "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==" }, @@ -3343,6 +3448,12 @@ "parseurl@1.3.3": { "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "path-exists@4.0.0": { + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-key@3.1.1": { + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, "path-to-regexp@0.1.12": { "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, @@ -3438,6 +3549,9 @@ "preact@10.19.6": { "integrity": "sha512-gympg+T2Z1fG1unB8NH29yHJwnEaCH37Z32diPDku316OTnRPeMbiRV9kTrfZpocXjdfnWuFUl/Mj4BHaf6gnw==" }, + "prelude-ls@1.2.1": { + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" + }, "process-warning@4.0.1": { "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==" }, @@ -3457,6 +3571,9 @@ "ipaddr.js@1.9.1" ] }, + "punycode@2.3.1": { + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + }, "pure-rand@6.1.0": { "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==" }, @@ -3570,6 +3687,9 @@ "requires-port@1.0.0": { "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "resolve-from@4.0.0": { + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, "resolve-pkg-maps@1.0.0": { "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==" }, @@ -3589,7 +3709,7 @@ "rfdc@1.4.1": { "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" }, - "rolldown-plugin-dts@0.13.14_rolldown@1.0.0-beta.54": { + "rolldown-plugin-dts@0.13.14_rolldown@1.0.0-beta.55": { "integrity": "sha512-wjNhHZz9dlN6PTIXyizB6u/mAg1wEFMW9yw7imEVe3CxHSRnNHVyycIX0yDEOVJfDNISLPbkCIPEpFpizy5+PQ==", "dependencies": [ "@babel/generator", @@ -3603,8 +3723,8 @@ "rolldown" ] }, - "rolldown@1.0.0-beta.54": { - "integrity": "sha512-3lIvjCWgjPL3gmiATUdV1NeVBGJZy6FdtwgLPol25tAkn46Q/MsVGfCSNswXwFOxGrxglPaN20IeALSIFuFyEg==", + "rolldown@1.0.0-beta.55": { + "integrity": "sha512-r8Ws43aYCnfO07ao0SvQRz4TBAtZJjGWNvScRBOHuiNHvjfECOJBIqJv0nUkL1GYcltjvvHswRilDF1ocsC0+g==", "dependencies": [ "@oxc-project/types", "@rolldown/pluginutils" @@ -3626,8 +3746,8 @@ ], "bin": true }, - "rollup@4.53.3": { - "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "rollup@4.53.5": { + "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==", "dependencies": [ "@types/estree" ], @@ -3706,26 +3826,26 @@ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "bin": true }, - "send@0.19.0": { - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "send@0.19.2": { + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "dependencies": [ "debug@2.6.9", "depd@2.0.0", "destroy", - "encodeurl@1.0.2", + "encodeurl@2.0.0", "escape-html", "etag", "fresh", - "http-errors@2.0.0", + "http-errors@2.0.1", "mime@1.6.0", "ms@2.1.3", "on-finished", "range-parser", - "statuses@2.0.1" + "statuses@2.0.2" ] }, - "serve-static@1.16.2": { - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "serve-static@1.16.3": { + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "dependencies": [ "encodeurl@2.0.0", "escape-html", @@ -3772,6 +3892,15 @@ ], "scripts": true }, + "shebang-command@2.0.0": { + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": [ + "shebang-regex" + ] + }, + "shebang-regex@3.0.0": { + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, "shiki@1.29.2": { "integrity": "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==", "dependencies": [ @@ -3866,9 +3995,6 @@ "statuses@1.5.0": { "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==" }, - "statuses@2.0.1": { - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - }, "statuses@2.0.2": { "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" }, @@ -3919,6 +4045,9 @@ "ansi-regex@6.2.2" ] }, + "strip-json-comments@3.1.1": { + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, "strtok3@10.3.4": { "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", "dependencies": [ @@ -4038,7 +4167,13 @@ "trim-lines@3.0.1": { "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==" }, - "tsdown@0.12.9_rolldown@1.0.0-beta.54": { + "ts-api-utils@2.1.0_typescript@5.9.3": { + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dependencies": [ + "typescript" + ] + }, + "tsdown@0.12.9_rolldown@1.0.0-beta.55": { "integrity": "sha512-MfrXm9PIlT3saovtWKf/gCJJ/NQCdE0SiREkdNC+9Qy6UHhdeDPxnkFaBD7xttVUmgp0yUHtGirpoLB+OVLuLA==", "dependencies": [ "ansis", @@ -4066,7 +4201,7 @@ "tsx@4.21.0": { "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dependencies": [ - "esbuild@0.27.1", + "esbuild", "get-tsconfig" ], "optionalDependencies": [ @@ -4074,6 +4209,12 @@ ], "bin": true }, + "type-check@0.4.0": { + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dependencies": [ + "prelude-ls" + ] + }, "type-fest@0.21.3": { "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==" }, @@ -4084,6 +4225,10 @@ "mime-types" ] }, + "typescript@5.9.3": { + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "bin": true + }, "ufo@1.6.1": { "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==" }, @@ -4125,6 +4270,9 @@ "undici-types@6.21.0": { "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" }, + "undici-types@7.10.0": { + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" + }, "undici-types@7.16.0": { "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==" }, @@ -4176,6 +4324,12 @@ "unpipe@1.0.0": { "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, + "uri-js@4.4.1": { + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": [ + "punycode" + ] + }, "uri-template-router@1.0.0": { "integrity": "sha512-WKcL9ZSIEhHE3f5P4Z47Tf0nWbcgV1ISb/OBuF8YKEYi0SQOyTLCzM6B/gAKFWZhRhqA+C/Ks8UXe2qU5W0FVg==" }, @@ -4221,11 +4375,11 @@ "vfile-message" ] }, - "vite@7.2.7_@types+node@22.19.2_tsx@4.21.0_yaml@2.8.2_picomatch@4.0.3": { - "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", + "vite@7.3.0_@types+node@22.19.3_tsx@4.21.0_yaml@2.8.2_picomatch@4.0.3": { + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dependencies": [ - "@types/node@22.19.2", - "esbuild@0.25.12", + "@types/node@22.19.3", + "esbuild", "fdir", "picomatch", "postcss", @@ -4238,13 +4392,13 @@ "fsevents" ], "optionalPeers": [ - "@types/node@22.19.2", + "@types/node@22.19.3", "tsx", "yaml" ], "bin": true }, - "vitefu@1.1.1_vite@7.2.7__@types+node@22.19.2__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_@types+node@22.19.2_tsx@4.21.0_yaml@2.8.2": { + "vitefu@1.1.1_vite@7.3.0__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_@types+node@22.19.3_tsx@4.21.0_yaml@2.8.2": { "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", "dependencies": [ "vite" @@ -4269,8 +4423,18 @@ "webidl-conversions" ] }, - "workerd@1.20251210.0": { - "integrity": "sha512-9MUUneP1BnRE9XAYi94FXxHmiLGbO75EHQZsgWqSiOXjoXSqJCw8aQbIEPxCy19TclEl/kHUFYce8ST2W+Qpjw==", + "which@2.0.2": { + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": [ + "isexe" + ], + "bin": true + }, + "word-wrap@1.2.5": { + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" + }, + "workerd@1.20251217.0": { + "integrity": "sha512-s3mHDSWwHTduyY8kpHOsl27ZJ4ziDBJlc18PfBvNMqNnhO7yBeemlxH7bo7yQyU1foJrIZ6IENHDDg0Z9N8zQA==", "optionalDependencies": [ "@cloudflare/workerd-darwin-64", "@cloudflare/workerd-darwin-arm64", @@ -4281,14 +4445,14 @@ "scripts": true, "bin": true }, - "wrangler@4.54.0_@cloudflare+workers-types@4.20251213.0_unenv@2.0.0-rc.24_workerd@1.20251210.0": { - "integrity": "sha512-bANFsjDwJLbprYoBK+hUDZsVbUv2SqJd8QvArLIcZk+fPq4h/Ohtj5vkKXD3k0s2bD1DXLk08D+hYmeNH+xC6A==", + "wrangler@4.56.0_@cloudflare+workers-types@4.20251219.0_unenv@2.0.0-rc.24_workerd@1.20251217.0": { + "integrity": "sha512-Nqi8duQeRbA+31QrD6QlWHW3IZVnuuRxMy7DEg46deUzywivmaRV/euBN5KKXDPtA24VyhYsK7I0tkb7P5DM2w==", "dependencies": [ "@cloudflare/kv-asset-handler", "@cloudflare/unenv-preset", "@cloudflare/workers-types", "blake3-wasm", - "esbuild@0.27.0", + "esbuild", "miniflare", "path-to-regexp@6.3.0", "unenv", @@ -4362,6 +4526,9 @@ "ylru@1.4.0": { "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==" }, + "yocto-queue@0.1.0": { + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, "yoctocolors-cjs@2.1.3": { "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==" }, @@ -4408,13 +4575,14 @@ "jsr:@std/testing@0.224", "jsr:@std/yaml@^1.0.8", "npm:@cloudflare/workers-types@^4.20250529.0", + "npm:@fxts/core@^1.21.1", "npm:@js-temporal/polyfill@~0.5.1", "npm:@nestjs/common@^11.0.1", "npm:@opentelemetry/api@^1.9.0", "npm:@types/node@^22.16.0", "npm:amqplib@~0.10.8", "npm:byte-encodings@^1.0.11", - "npm:es-toolkit@^1.39.10", + "npm:es-toolkit@^1.42.0", "npm:h3@^1.15.0", "npm:ioredis@^5.6.1", "npm:json-preserve-indent@^1.1.3", @@ -4451,7 +4619,6 @@ "jsr:@hono/hono@^4.8.3", "jsr:@optique/core@~0.6.1", "jsr:@optique/run@~0.6.1", - "npm:@fxts/core@^1.15.0", "npm:@inquirer/prompts@^7.8.4", "npm:@jimp/core@^1.6.0", "npm:@jimp/wasm-webp@^1.6.0", @@ -4563,6 +4730,21 @@ "npm:koa@2" ] }, + "packages/lint": { + "dependencies": [ + "npm:@types/estree@^1.0.8", + "npm:eslint@9" + ], + "packageJson": { + "dependencies": [ + "npm:@types/eslint@9", + "npm:@types/estree@^1.0.8", + "npm:@typescript-eslint/parser@^8.49.0", + "npm:@typescript-eslint/utils@8", + "npm:eslint@9" + ] + } + }, "packages/relay": { "dependencies": [ "jsr:@std/assert@^1.0.13" diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 8123fa48d..b4414692c 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -76,6 +76,7 @@ const MANUAL = { { text: "Message queue", link: "/manual/mq.md" }, { text: "Integration", link: "/manual/integration.md" }, { text: "Testing", link: "/manual/test.md" }, + { text: "Linting", link: "/manual/lint.md" }, { text: "Logging", link: "/manual/log.md" }, { text: "OpenTelemetry", link: "/manual/opentelemetry.md" }, { text: "Deployment", link: "/manual/deploy.md" }, @@ -100,6 +101,7 @@ const REFERENCES = { { text: "@fedify/sqlite", link: "https://jsr.io/@fedify/sqlite/doc" }, { text: "@fedify/sveltekit", link: "https://jsr.io/@fedify/sveltekit/doc" }, { text: "@fedify/testing", link: "https://jsr.io/@fedify/testing/doc" }, + { text: "@fedify/lint", link: "https://jsr.io/@fedify/lint/doc" }, ], }; diff --git a/docs/manual/lint.md b/docs/manual/lint.md new file mode 100644 index 000000000..569cb1637 --- /dev/null +++ b/docs/manual/lint.md @@ -0,0 +1,1143 @@ +--- +description: >- + Fedify provides linting plugins for Deno Lint and ESLint to help you catch + common mistakes and enforce best practices when building federated server + apps. +--- + +Linting +======= + +_This package is available since Fedify 2.0.0._ + +> [!TIP] +> We highly recommend using the `@fedify/lint` package in your federated server +> app to catch common mistakes early and enforce best practices. + +Fedify provides the [`@fedify/lint`] package, which includes lint rules +specifically designed for Fedify applications. It supports both [Deno Lint] and +[ESLint], so you can use it regardless of your JavaScript/TypeScript runtime. + +The plugin includes rules that check for: + +- Proper actor ID configuration +- Required actor properties (inbox, outbox, followers, etc.) +- Correct URL patterns for actor collections +- Public key and assertion method requirements +- Collection filtering implementation + +[`@fedify/lint`]: https://jsr.io/@fedify/lint +[Deno Lint]: https://docs.deno.com/runtime/reference/lint_plugins/ +[ESLint]: https://eslint.org/ + +Installation +------------ + +::: code-group + +~~~~sh [Deno] +deno add jsr:@fedify/lint +~~~~ + +~~~~sh [npm] +npm add -D @fedify/lint +~~~~ + +~~~~sh [pnpm] +pnpm add -D @fedify/lint +~~~~ + +~~~~sh [Yarn] +yarn add -D @fedify/lint +~~~~ + +~~~~sh [Bun] +bun add -D @fedify/lint +~~~~ + +::: + +Deno Lint +--------- + +### Basic setup + +Add the plugin to your _deno.json_ configuration file: + +~~~~json +{ + "lint": { + "plugins": ["jsr:@fedify/lint"] + } +} +~~~~ + +By default, this enables all recommended rules. + +### Custom configuration + +You can customize which rules to enable and their severity levels: + +~~~~json +{ + "lint": { + "plugins": ["jsr:@fedify/lint"], + "rules": { + "tags": ["recommended"], + "include": [ + "@fedify/lint/actor-id-required", + "@fedify/lint/actor-id-mismatch" + ], + "exclude": [ + "@fedify/lint/actor-featured-property-required" + ] + } + } +} +~~~~ + +### Running Deno Lint + +After setting up the configuration, run Deno's linter: + +~~~~sh +deno lint +~~~~ + +You can also specify which files to lint: + +~~~~sh +deno lint federation.ts +deno lint src/federation/ +~~~~ + +ESLint +------ + +### Basic setup + +Add the plugin to your ESLint configuration file (e.g., _eslint.config.ts_ or +_eslint.config.js_): + +~~~~ typescript twoslash +import fedifyLint from "@fedify/lint"; + +// If your `createFederation` code is in `federation.ts` or `federation/**.ts` +export default fedifyLint; +~~~~ + +Or specify your own federation files: + +~~~~ typescript twoslash +// @errors: 2304 +import fedifyLint from "@fedify/lint"; +// ---cut-before--- +export default { + ...fedifyLint, + files: ["my-own-federation.ts"], +}; +~~~~ + +If you use other ESLint configurations: + +~~~~ typescript twoslash +// @errors: 2304 +import fedifyLint from "@fedify/lint"; +// ---cut-before--- +export default [ + // otherConfig, + fedifyLint, +]; +~~~~ + +The default configuration applies recommended rules to files that match common +federation-related patterns (e.g., _federation.ts_, _federation/\*.ts_). + +### Custom configuration + +You can customize which files to lint and which rules to enable: + +~~~~ typescript twoslash +import { plugin } from "@fedify/lint"; + +export default [{ + files: ["src/federation/**/*.ts"], // Your federation code location + plugins: { + "@fedify/lint": plugin, + }, + rules: { + "@fedify/lint/actor-id-required": "error", + "@fedify/lint/actor-id-mismatch": "error", + "@fedify/lint/actor-inbox-property-required": "warn", + // ... other rules + }, +}]; +~~~~ + +### Using configurations + +The plugin provides two preset configurations: + +#### Recommended (default) + +Enables critical rules as errors and optional rules as warnings: + +~~~~ typescript twoslash +import fedifyLint from "@fedify/lint"; + +export default fedifyLint; +~~~~ + +#### Strict + +Enables all rules as errors: + +~~~~ typescript twoslash +import { plugin } from "@fedify/lint"; + +export default [{ + files: ["**/*.ts"], + ...plugin.configs.strict, +}]; +~~~~ + +### Running ESLint + +Set up your ESLint configuration as shown above and add a script to +_package.json_: + +~~~~jsonc +{ + "scripts": { + "lint": "eslint ." + } +} +~~~~ + +After setting up the configuration, run ESLint on your codebase: + +::: code-group + +~~~~sh [npm] +npm run lint +~~~~ + +~~~~sh [pnpm] +pnpm lint +~~~~ + +~~~~sh [Yarn] +yarn lint +~~~~ + +~~~~sh [Bun] +bun lint +~~~~ + +::: + +Or run the linter directly via command line: + +::: code-group + +~~~~sh [npm] +npx eslint . +~~~~ + +~~~~sh [pnpm] +pnpx eslint . +~~~~ + +~~~~sh [Yarn] +yarn eslint . +~~~~ + +~~~~sh [Bun] +bunx eslint . +~~~~ + +::: + +Rules +----- + +### `actor-id-required` + +Ensures all actors have an `id` property in the actor dispatcher. + +**When this rule applies:** +The actor dispatcher returns a `Person`, `Organization`, `Group`, `Application`, +or `Service` object without an `id` property. + +**Why it matters:** +Every ActivityPub actor must have a unique identifier (ID) to be discoverable +and to receive activities from other servers. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing id property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + name: "John Doe", // No id! + }); +}); + +// ✅ Good: Include id property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + }); +}); +~~~~ + +### `actor-id-mismatch` + +Validates that actor IDs match the expected URI from `Context.getActorUri()`. + +**When this rule applies:** +The `id` property is set to a value other than `ctx.getActorUri(identifier)`, +such as a hardcoded URL string, `new URL(...)`, or a different context method. + +**Why it matters:** +Using the wrong URI for the actor ID can cause federation issues. Other servers +won't be able to properly verify the actor's identity or send activities to it. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using hardcoded URL +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: new URL(`https://example.com/users/${identifier}`), + name: "John Doe", + }); +}); + +// ❌ Bad: Using wrong context method +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getFollowersUri(identifier), // Wrong method! + name: "John Doe", + }); +}); + +// ✅ Good: Use ctx.getActorUri() +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + }); +}); +~~~~ + +### `actor-public-key-required` + +Ensures actors have public keys for [HTTP Signatures]. + +**When this rule applies:** +The actor dispatcher is chained with `setKeyPairsDispatcher()`, but the actor +object doesn't include a `publicKey` or `publicKeys` property. + +**Why it matters:** +HTTP Signatures are used to verify the authenticity of activities. Without +a public key, other servers cannot verify that activities sent by your actor +are legitimate. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing publicKey when setKeyPairsDispatcher is configured +federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + // Missing publicKey! + }); + }) + .setKeyPairsDispatcher(async (ctx, identifier) => { + // Returns key pairs... + return []; + }); + +// ✅ Good: Include publicKey from key pairs dispatcher +federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const keyPairs = await ctx.getActorKeyPairs(identifier); + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + publicKey: keyPairs[0].cryptographicKey, + }); + }) + .setKeyPairsDispatcher(async (ctx, identifier) => { + // Returns key pairs... + return []; + }); +~~~~ + +[HTTP Signatures]: ./send.md#http-signatures + +### `actor-assertion-method-required` + +Validates that actors have assertion methods for [Object Integrity Proofs]. + +**When this rule applies:** +The actor dispatcher is chained with `setKeyPairsDispatcher()`, but the actor +object doesn't include an `assertionMethod` property. + +**Why it matters:** +Object Integrity Proofs use assertion methods to cryptographically sign +activities. This provides an additional layer of security beyond HTTP +Signatures. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing assertionMethod when setKeyPairsDispatcher is configured +federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const keyPairs = await ctx.getActorKeyPairs(identifier); + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + publicKey: keyPairs[0].cryptographicKey, + // Missing assertionMethod! + }); + }) + .setKeyPairsDispatcher(async (ctx, identifier) => { + // Returns key pairs... + return []; + }); + +// ✅ Good: Include assertionMethod from key pairs dispatcher +federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const keyPairs = await ctx.getActorKeyPairs(identifier); + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + publicKey: keyPairs[0].cryptographicKey, + assertionMethod: keyPairs[0].multikey, + }); + }) + .setKeyPairsDispatcher(async (ctx, identifier) => { + // Returns key pairs... + return []; + }); +~~~~ + +[Object Integrity Proofs]: ./send.md#object-integrity-proofs + +### `actor-inbox-property-required` + +Ensures `inbox` is defined when `setInboxListeners()` is configured. + +**When this rule applies:** +You've called `federation.setInboxListeners()` to handle incoming activities, +but the actor object doesn't include an `inbox` property. + +**Why it matters:** +The inbox URL tells other servers where to send activities to your actor. +Without it, your actor cannot receive follow requests, mentions, or any other +activities. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing inbox when setInboxListeners is configured +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + // Missing inbox! + }); +}); + +federation.setInboxListeners("/users/{identifier}/inbox", "/inbox"); + +// ✅ Good: Include inbox property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + inbox: ctx.getInboxUri(identifier), + }); +}); + +federation.setInboxListeners("/users/{identifier}/inbox", "/inbox"); +~~~~ + +### `actor-inbox-property-mismatch` + +Validates that the `inbox` URI is set using `ctx.getInboxUri(identifier)`. + +**When this rule applies:** +The `inbox` property is set to a value other than `ctx.getInboxUri(identifier)`. + +**Why it matters:** +The inbox URI must match the path configured in `setInboxListeners()`. Using +a different URI will cause incoming activities to fail. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using hardcoded URL +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + inbox: new URL(`https://example.com/inbox/${identifier}`), // Wrong! + }); +}); + +// ✅ Good: Use ctx.getInboxUri() +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + inbox: ctx.getInboxUri(identifier), + }); +}); +~~~~ + +### `actor-outbox-property-required` + +Ensures `outbox` is defined when `setOutboxDispatcher()` is configured. + +**When this rule applies:** +You've called `federation.setOutboxDispatcher()` to serve the actor's outbox, +but the actor object doesn't include an `outbox` property. + +**Why it matters:** +The outbox URL allows other servers and users to view the actor's published +activities. It's part of the standard ActivityPub actor profile. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing outbox when setOutboxDispatcher is configured +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + // Missing outbox! + }); +}); + +federation.setOutboxDispatcher( + "/users/{identifier}/outbox", + (ctx, identifier) => ({ items: [] }) +); + +// ✅ Good: Include outbox property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + outbox: ctx.getOutboxUri(identifier), + }); +}); +~~~~ + +### `actor-outbox-property-mismatch` + +Validates that the `outbox` URI is set using `ctx.getOutboxUri(identifier)`. + +**When this rule applies:** +The `outbox` property is set to a value other than +`ctx.getOutboxUri(identifier)`. + +**Why it matters:** +The outbox URI must match the path configured in `setOutboxDispatcher()`. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using wrong context method +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + outbox: ctx.getInboxUri(identifier), // Wrong method! + }); +}); + +// ✅ Good: Use ctx.getOutboxUri() +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + outbox: ctx.getOutboxUri(identifier), + }); +}); +~~~~ + +### `actor-followers-property-required` + +Ensures `followers` is defined when `setFollowersDispatcher()` is configured. + +**When this rule applies:** +You've called `federation.setFollowersDispatcher()` to serve the actor's +followers collection, but the actor object doesn't include a `followers` +property. + +**Why it matters:** +The followers URL allows other servers to discover who follows this actor, +which is important for activity delivery and social graph discovery. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing followers when setFollowersDispatcher is configured +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + // Missing followers! + }); +}); + +federation.setFollowersDispatcher( + "/users/{identifier}/followers", + (ctx, identifier) => ({ items: [] }) +); + +// ✅ Good: Include followers property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + followers: ctx.getFollowersUri(identifier), + }); +}); +~~~~ + +### `actor-followers-property-mismatch` + +Validates that the `followers` URI is set using +`ctx.getFollowersUri(identifier)`. + +**When this rule applies:** +The `followers` property is set to a value other than +`ctx.getFollowersUri(identifier)`. + +**Why it matters:** +The followers URI must match the path configured in `setFollowersDispatcher()`. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using wrong context method +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + followers: ctx.getFollowingUri(identifier), // Wrong method! + }); +}); + +// ✅ Good: Use ctx.getFollowersUri() +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + followers: ctx.getFollowersUri(identifier), + }); +}); +~~~~ + +### `actor-following-property-required` + +Ensures `following` is defined when `setFollowingDispatcher()` is configured. + +**When this rule applies:** +You've called `federation.setFollowingDispatcher()` to serve the actor's +following collection, but the actor object doesn't include a `following` +property. + +**Why it matters:** +The following URL allows other servers to discover who this actor follows. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing following when setFollowingDispatcher is configured +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + // Missing following! + }); +}); + +federation.setFollowingDispatcher( + "/users/{identifier}/following", + (ctx, identifier) => ({ items: [] }) +); + +// ✅ Good: Include following property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + following: ctx.getFollowingUri(identifier), + }); +}); +~~~~ + +### `actor-following-property-mismatch` + +Validates that the `following` URI is set using +`ctx.getFollowingUri(identifier)`. + +**When this rule applies:** +The `following` property is set to a value other than +`ctx.getFollowingUri(identifier)`. + +**Why it matters:** +The following URI must match the path configured in `setFollowingDispatcher()`. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using wrong context method +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + following: ctx.getFollowersUri(identifier), // Wrong method! + }); +}); + +// ✅ Good: Use ctx.getFollowingUri() +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + following: ctx.getFollowingUri(identifier), + }); +}); +~~~~ + +### `actor-liked-property-required` + +Ensures `liked` is defined when `setLikedDispatcher()` is configured. + +**When this rule applies:** +You've called `federation.setLikedDispatcher()` to serve the actor's liked +collection, but the actor object doesn't include a `liked` property. + +**Why it matters:** +The liked URL allows other servers to discover what content this actor has +liked. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing liked when setLikedDispatcher is configured +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + // Missing liked! + }); +}); + +federation.setLikedDispatcher( + "/users/{identifier}/liked", + (ctx, identifier) => ({ items: [] }) +); + +// ✅ Good: Include liked property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + liked: ctx.getLikedUri(identifier), + }); +}); +~~~~ + +### `actor-liked-property-mismatch` + +Validates that the `liked` URI is set using `ctx.getLikedUri(identifier)`. + +**When this rule applies:** +The `liked` property is set to a value other than `ctx.getLikedUri(identifier)`. + +**Why it matters:** +The liked URI must match the path configured in `setLikedDispatcher()`. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using wrong context method +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + liked: ctx.getFollowersUri(identifier), // Wrong method! + }); +}); + +// ✅ Good: Use ctx.getLikedUri() +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + liked: ctx.getLikedUri(identifier), + }); +}); +~~~~ + +### `actor-featured-property-required` + +Ensures `featured` is defined when `setFeaturedDispatcher()` is configured. + +**When this rule applies:** +You've called `federation.setFeaturedDispatcher()` to serve the actor's +featured/pinned posts collection, but the actor object doesn't include a +`featured` property. + +**Why it matters:** +The featured URL allows other servers to discover the actor's pinned or +highlighted content (commonly shown at the top of a profile). + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing featured when setFeaturedDispatcher is configured +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + // Missing featured! + }); +}); + +federation.setFeaturedDispatcher( + "/users/{identifier}/featured", + (ctx, identifier) => ({ items: [] }) +); + +// ✅ Good: Include featured property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + featured: ctx.getFeaturedUri(identifier), + }); +}); +~~~~ + +### `actor-featured-property-mismatch` + +Validates that the `featured` URI is set using +`ctx.getFeaturedUri(identifier)`. + +**When this rule applies:** +The `featured` property is set to a value other than +`ctx.getFeaturedUri(identifier)`. + +**Why it matters:** +The featured URI must match the path configured in `setFeaturedDispatcher()`. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using wrong context method +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + featured: ctx.getFollowersUri(identifier), // Wrong method! + }); +}); + +// ✅ Good: Use ctx.getFeaturedUri() +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + featured: ctx.getFeaturedUri(identifier), + }); +}); +~~~~ + +### `actor-featured-tags-property-required` + +Ensures `featuredTags` is defined when `setFeaturedTagsDispatcher()` is +configured. + +**When this rule applies:** +You've called `federation.setFeaturedTagsDispatcher()` to serve the actor's +featured hashtags collection, but the actor object doesn't include a +`featuredTags` property. + +**Why it matters:** +The featuredTags URL allows other servers to discover the actor's featured +hashtags (commonly used for profile discovery). + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing featuredTags when setFeaturedTagsDispatcher is configured +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + // Missing featuredTags! + }); +}); + +federation.setFeaturedTagsDispatcher( + "/users/{identifier}/tags", + (ctx, identifier) => ({ items: [] }) +); + +// ✅ Good: Include featuredTags property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + featuredTags: ctx.getFeaturedTagsUri(identifier), + }); +}); +~~~~ + +### `actor-featured-tags-property-mismatch` + +Validates that the `featuredTags` URI is set using +`ctx.getFeaturedTagsUri(identifier)`. + +**When this rule applies:** +The `featuredTags` property is set to a value other than +`ctx.getFeaturedTagsUri(identifier)`. + +**Why it matters:** +The featuredTags URI must match the path configured in +`setFeaturedTagsDispatcher()`. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using wrong context method +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + featuredTags: ctx.getFollowersUri(identifier), // Wrong method! + }); +}); + +// ✅ Good: Use ctx.getFeaturedTagsUri() +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + featuredTags: ctx.getFeaturedTagsUri(identifier), + }); +}); +~~~~ + +### `actor-shared-inbox-property-required` + +Ensures `endpoints.sharedInbox` is defined when `setInboxListeners()` is +configured with a shared inbox path. + +**When this rule applies:** +You've called `federation.setInboxListeners()` with a second parameter (shared +inbox path), but the actor object doesn't include an +`endpoints: new Endpoints({ sharedInbox: ... })` property. + +**Why it matters:** +The shared inbox allows other servers to send activities to multiple actors +on your server with a single request, improving federation efficiency. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Endpoints, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing sharedInbox when setInboxListeners has shared inbox path +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + inbox: ctx.getInboxUri(identifier), + // Missing endpoints.sharedInbox! + }); +}); + +federation.setInboxListeners("/users/{identifier}/inbox", "/inbox"); + +// ✅ Good: Include endpoints.sharedInbox +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + inbox: ctx.getInboxUri(identifier), + endpoints: new Endpoints({ + sharedInbox: ctx.getInboxUri(), + }), + }); +}); +~~~~ + +### `actor-shared-inbox-property-mismatch` + +Validates that `endpoints.sharedInbox` is set using `ctx.getInboxUri()` (without +identifier). + +**When this rule applies:** +The `endpoints.sharedInbox` property is set to a value other than +`ctx.getInboxUri()` (called without arguments for the shared inbox). + +**Why it matters:** +The shared inbox URI must match the shared inbox path configured in +`setInboxListeners()`. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Endpoints, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using getInboxUri with identifier for shared inbox +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + inbox: ctx.getInboxUri(identifier), + endpoints: new Endpoints({ + sharedInbox: ctx.getInboxUri(identifier), // Wrong! Should be no args + }), + }); +}); + +// ✅ Good: Use ctx.getInboxUri() without arguments +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + inbox: ctx.getInboxUri(identifier), + endpoints: new Endpoints({ + sharedInbox: ctx.getInboxUri(), // No identifier for shared inbox + }), + }); +}); +~~~~ + +### `collection-filtering-not-implemented` + +Warns when collection dispatchers don't implement filtering. + +**When this rule applies:** +The `setFollowersDispatcher()` callback function has fewer than 4 parameters +(missing the `filter` parameter). + +> [!NOTE] +> Currently, this rule only checks `setFollowersDispatcher()`. Other collection +> dispatchers may be added in the future. + +**Why it matters:** +Collection filtering allows clients to request specific subsets of a collection, +reducing response payload sizes and improving performance. Without filtering, +large collections could cause performance issues. + +For more information, see the [*Filtering by server*] section in the +collections manual. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing filter parameter +federation.setFollowersDispatcher( + "/users/{identifier}/followers", + async (ctx, identifier, cursor) => { // Only 3 parameters! + return { items: [] }; + } +); + +// ✅ Good: Include filter parameter (4th parameter) +federation.setFollowersDispatcher( + "/users/{identifier}/followers", + async (ctx, identifier, cursor, filter) => { + // Use filter to handle filtering requests + return { items: [] }; + } +); +~~~~ + +[*Filtering by server*]: ./collections.md#filtering-by-server + +Example +------- + +Here's an example of code that would trigger lint errors: + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Wrong: Using relative URL for actor ID +federation.setActorDispatcher( + "/{identifier}", + (_ctx, identifier) => { + return new Person({ + id: new URL(`/${identifier}`), // ❌ Should use ctx.getActorUri() + name: "Example User", + }); + }, +); +~~~~ + +Corrected version: + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ✅ Correct: Using Context.getActorUri() for actor ID +federation.setActorDispatcher( + "/{identifier}", + (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), // ✅ Correct + name: "Example User", + inbox: ctx.getInboxUri(identifier), + outbox: ctx.getOutboxUri(identifier), + followers: ctx.getFollowersUri(identifier), + // ... other required properties + }); + }, +); +~~~~ + +When you run the linter on the incorrect code, you'll see an error like: + +~~~~ +error[fedify-lint/actor-id-mismatch]: Actor's `id` property must match +`ctx.getActorUri(identifier)`. Ensure you're using the correct context method. +~~~~ + +See also +-------- + +- [`@fedify/lint` on JSR](https://jsr.io/@fedify/lint) +- [`@fedify/lint` on npm](https://www.npmjs.com/package/@fedify/lint) +- [Deno Lint plugins documentation](https://docs.deno.com/runtime/reference/lint_plugins/) +- [ESLint documentation](https://eslint.org/) +- [Example project](https://github.com/fedify-dev/fedify/tree/main/examples/lint) diff --git a/docs/package.json b/docs/package.json index e1e8ad353..8498c23cf 100644 --- a/docs/package.json +++ b/docs/package.json @@ -11,6 +11,7 @@ "@fedify/h3": "workspace:", "@fedify/hono": "workspace:", "@fedify/koa": "workspace:", + "@fedify/lint": "workspace:", "@fedify/nestjs": "workspace:", "@fedify/next": "workspace:", "@fedify/postgres": "workspace:", diff --git a/docs/tutorial/basics.md b/docs/tutorial/basics.md index 2d64b2736..ced599ee8 100644 --- a/docs/tutorial/basics.md +++ b/docs/tutorial/basics.md @@ -1279,6 +1279,53 @@ should see the actor's followers in the bulleted list. [Hono]: https://hono.dev/ +Linting your federation code +----------------------------- + +As your federated server grows, it's important to maintain code quality and +catch common mistakes early. Fedify provides the [`@fedify/lint`] package +with linting plugins for both Deno Lint and ESLint, which include specialized +linting rules for federation code. + +The `@fedify/lint` package helps you avoid common pitfalls such as: + + - Using incorrect actor IDs (e.g., relative URLs instead of + `Context.getActorUri()`) + - Missing required actor properties (inbox, outbox, followers, etc.) + - Incorrect URL patterns for actor collections + - Missing public keys or assertion methods + +For example, if you had written the actor dispatcher like this: + +~~~~ typescript +// ❌ Wrong: Using relative URL +federation.setActorDispatcher( + "/{identifier}", + (_ctx, identifier) => { + return new Person({ + id: new URL(`/${identifier}`), // ❌ Linter will catch this + name: "Example User", + }); + }, +); +~~~~ + +The linter would warn you: + +~~~~ +error[fedify-lint/actor-id-mismatch]: Actor's `id` property must match +`_ctx.getActorUri(identifier)`. Ensure you're using the correct context method. +~~~~ + +This helps you catch mistakes before they cause issues in production. + +> [!TIP] +> For detailed setup instructions, available rules, and configuration options, +> see the [*Linting* section](../manual/lint.md) in the manual. + +[`@fedify/lint`]: https://jsr.io/@fedify/lint + + Wrapping up ----------- diff --git a/packages/cli/deno.json b/packages/cli/deno.json index 9fa848cb8..9aa1666c3 100644 --- a/packages/cli/deno.json +++ b/packages/cli/deno.json @@ -4,7 +4,6 @@ "license": "MIT", "exports": "./src/mod.ts", "imports": { - "@fxts/core": "npm:@fxts/core@^1.15.0", "@hongminhee/localtunnel": "jsr:@hongminhee/localtunnel@^0.3.0", "@inquirer/prompts": "npm:@inquirer/prompts@^7.8.4", "@jimp/core": "npm:@jimp/core@^1.6.0", diff --git a/packages/lint/README.md b/packages/lint/README.md new file mode 100644 index 000000000..fd9385a56 --- /dev/null +++ b/packages/lint/README.md @@ -0,0 +1,410 @@ + + +@fedify/lint: ESLint plugin for Fedify +======================================= + +[![JSR][JSR badge]][JSR] +[![npm][npm badge]][npm] +[![Follow @fedify@hollo.social][@fedify@hollo.social badge]][@fedify@hollo.social] + +*This package is available since Fedify 2.0.0.* + +This package provides [Deno Lint] and [ESLint] plugin with lint rules specifically designed +for [Fedify] applications. It helps you catch common mistakes and enforce +best practices when building federated server apps with Fedify. + +The plugin includes rules that check for: + + - Proper actor ID configuration + - Required actor properties (inbox, outbox, followers, etc.) + - Correct URL patterns for actor collections + - Public key and assertion method requirements + - Collection filtering implementation + + +### Deno Lint Configuration Example + +~~~~ typescript +// deno.json + +{ + "lint": { + "plugins": { + "@fedify/lint": "jsr:@fedify/lint" + }, + "rules": { + "@fedify/lint/actor-id-required": "error", + "@fedify/lint/actor-id-mismatch": "error", + "@fedify/lint/actor-inbox-property-required": "warn" + // ... other rules + } + } +} +~~~~ + +### ESLint Configuration Example + +~~~~ typescript +// eslint.config.ts + +import fedifyLint from "@fedify/lint"; + +export default fedifyLint; +~~~~ + +[JSR]: https://jsr.io/@fedify/lint +[JSR badge]: https://jsr.io/badges/@fedify/lint +[npm]: https://www.npmjs.com/package/@fedify/lint +[npm badge]: https://img.shields.io/npm/v/@fedify/lint?logo=npm +[@fedify@hollo.social badge]: https://fedi-badge.deno.dev/@fedify@hollo.social/followers.svg +[@fedify@hollo.social]: https://hollo.social/@fedify +[Deno Lint]: https://docs.deno.com/runtime/reference/lint_plugins/ +[ESLint]: https://eslint.org/ +[Fedify]: https://fedify.dev/ + + +Features +-------- + +The `@fedify/lint` package provides comprehensive linting rules for Fedify +federation code: + +### Actor Validation Rules + + - **`actor-id-required`**: Ensures all actors have an `id` property + - **`actor-id-mismatch`**: Validates that actor IDs match the expected URI + from `Context.getActorUri()` + - **`actor-public-key-required`**: Ensures actors have public keys for + HTTP Signatures + - **`actor-assertion-method-required`**: Validates assertion methods for + Object Integrity Proofs + +### Collection Property Rules + + - **`actor-inbox-property-required`**: Ensures inbox is defined when + `setInboxListeners` is set + - **`actor-inbox-property-mismatch`**: Validates inbox URI from `getInboxUri` + - **`actor-outbox-property-required`**: Ensures outbox is defined when + `setOutboxDispatcher` is set + - **`actor-outbox-property-mismatch`**: Validates outbox URI from + `getOutboxUri` + - **`actor-followers-property-required`**: Ensures followers is defined when + `setFollowersDispatcher` is set + - **`actor-followers-property-mismatch`**: Validates followers URI from + `getFollowersUri` + - **`actor-following-property-required`**: Ensures following is defined when + `setFollowingDispatcher` is set + - **`actor-following-property-mismatch`**: Validates following URI from + `getFollowingUri` + - **`actor-liked-property-required`**: Ensures liked is defined when + `setLikedDispatcher` is set + - **`actor-liked-property-mismatch`**: Validates liked URI from `getLikedUri` + - **`actor-featured-property-required`**: Ensures featured is defined when + `setFeaturedDispatcher` is set + - **`actor-featured-property-mismatch`**: Validates featured URI from + `getFeaturedUri` + - **`actor-featured-tags-property-required`**: Ensures featuredTags is defined + when `setFeaturedTagsDispatcher` is set + - **`actor-featured-tags-property-mismatch`**: Validates featuredTags URI from + `getFeaturedTagsUri` + - **`actor-shared-inbox-property-required`**: Ensures sharedInbox is defined + when `setInboxListeners` is set + - **`actor-shared-inbox-property-mismatch`**: Validates sharedInbox URI from + `getInboxUri` + +### Other Rules + + - **`collection-filtering-not-implemented`**: Warns about missing collection + filtering implementation (`setFollowersDispatcher` only for now) + + +Installation +------------ + +::: code-group + +~~~~ sh [Deno] +deno add jsr:@fedify/lint +~~~~ + +~~~~ sh [npm] +npm add -D @fedify/lint +~~~~ + +~~~~ sh [pnpm] +pnpm add -D @fedify/lint +~~~~ + +~~~~ sh [Yarn] +yarn add -D @fedify/lint +~~~~ + +~~~~ sh [Bun] +bun add -D @fedify/lint +~~~~ + +::: + + +Usage (Deno Lint) +------------------ + +### Basic Setup + +Add the plugin to your *deno.json* configuration file: + +~~~~ json +{ + "lint": { + "plugins": ["jsr:@fedify/lint"] + } +} +~~~~ + +By default, this enables all recommended rules. + +### Custom Configuration + +You can customize which rules to enable and their severity levels: + +~~~~ json +{ + "lint": { + "plugins": ["jsr:@fedify/lint"], + "rules": { + "tags": ["recommended"], + "include": [ + "@fedify/lint/actor-id-required", + "@fedify/lint/actor-id-mismatch" + ], + "exclude": [ + "@fedify/lint/actor-featured-property-required" + ] + } + } +} +~~~~ + +### Running Deno Lint + +After setting up the configuration, run Deno's linter: + +~~~~ sh +deno lint +~~~~ + +You can also specify which files to lint: + +~~~~ sh +deno lint federation.ts +deno lint src/federation/ +~~~~ + + +Usage (ESLint) +-------------- + +### Basic Setup + +Add the plugin to your ESLint configuration file (e.g., *eslint.config.ts* +or *eslint.config.js*): + +~~~~ typescript +import fedifyLint from "@fedify/lint"; + +// If your `createFederation` code is in `federation.ts` or `federation/**.ts` +export default fedifyLint; + +// Or specify your own federation files +export default { + ...fedifyLint, + files: ["my-own-federation.ts"], +}; + +// If you use other ESLint configurations + +export default [ + otherConfig, + fedifyLint, +]; +~~~~ + +The default configuration applies recommended rules to files that match +common federation-related patterns (e.g., *federation.ts*, *federation/\*.ts*). + +### Custom Configuration + +You can customize which files to lint and which rules to enable: + +~~~~ typescript +import { plugin } from "@fedify/lint"; + +export default [{ + files: ["src/federation/**/*.ts"], // Your federation code location + plugins: { + "@fedify/lint": plugin, + }, + rules: { + "@fedify/lint/actor-id-required": "error", + "@fedify/lint/actor-id-mismatch": "error", + "@fedify/lint/actor-inbox-property-required": "warn", + // ... other rules + }, +}]; +~~~~ + +### Using Configurations + +The plugin provides two preset configurations: + +#### Recommended (default) + +Enables critical rules as errors and optional rules as warnings: + +~~~~ typescript +import fedifyLint from "@fedify/lint"; + +export default fedifyLint; +~~~~ + +#### Strict + +Enables all rules as errors: + +~~~~ typescript +import { plugin } from "@fedify/lint"; + +export default [{ + files: ["**/*.ts"], + ...plugin.configs.strict, +}]; +~~~~ + + +Example +------- + +Here's an example of code that would trigger lint errors: + +~~~~ typescript +// ❌ Wrong: Using relative URL for actor ID +import { createFederation, Person } from "@fedify/fedify"; + +const federation = createFederation({ /* ... */ }); + +federation.setActorDispatcher( + "/{identifier}", + (_ctx, identifier) => { + return new Person({ + id: new URL(`/${identifier}`), // ❌ Should use ctx.getActorUri() + name: "Example User", + }); + }, +); +~~~~ + +Corrected version: + +~~~~ typescript +// ✅ Correct: Using Context.getActorUri() for actor ID +import { createFederation, Person } from "@fedify/fedify"; + +const federation = createFederation({ /* ... */ }); + +federation.setActorDispatcher( + "/{identifier}", + (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), // ✅ Correct + name: "Example User", + inbox: ctx.getInboxUri(identifier), + outbox: ctx.getOutboxUri(identifier), + followers: ctx.getFollowersUri(identifier), + // ... other required properties + }); + }, +); +~~~~ + + +Running the Linter +------------------ + +### Deno Lint + +Run Deno's linter with the plugin enabled: + +~~~~ sh +deno lint +~~~~ + +You can also specify which files or directories to lint: + +~~~~ sh +deno lint federation.ts +deno lint src/federation/ +~~~~ + +### ESLint + +Set up your ESLint configuration as shown above and add a follwing script on +`package.json`: + +~~~~ jsonc +{ + "scripts": { + "lint": "eslint ." + } +} +~~~~ + +After setting up the configuration, run ESLint on your codebase: + +::: code-group + +~~~~ sh [npm] +npm run lint +~~~~ + +~~~~ sh [pnpm] +pnpm lint +~~~~ + +~~~~ sh [Yarn] +yarn lint +~~~~ + +~~~~ sh [Bun] +bun lint +~~~~ + +::: + +or run the linter directly via command line: + +::: code-group + +~~~~ sh [npm] +npx eslint . +~~~~ + +~~~~ sh [pnpm] +pnpx eslint . +~~~~ + +~~~~ sh [Yarn] +yarn eslint . +~~~~ + +~~~~ sh [Bun] +bunx eslint . +~~~~ + +::: + +See Also +-------- + + - [Fedify Documentation](https://fedify.dev/) + - [ESLint Documentation](https://eslint.org/) + - [Example Project](https://github.com/fedify-dev/fedify/tree/main/examples/lint) diff --git a/packages/lint/deno.json b/packages/lint/deno.json new file mode 100644 index 000000000..1b3c8f468 --- /dev/null +++ b/packages/lint/deno.json @@ -0,0 +1,15 @@ +{ + "name": "@fedify/lint", + "version": "2.0.0", + "license": "MIT", + "exports": { + ".": "./src/mod.ts" + }, + "imports": { + "eslint": "npm:eslint@^9.0.0", + "estree": "npm:@types/estree@^1.0.8" + }, + "tasks": { + "test": "deno test --allow-env" + } +} diff --git a/packages/lint/package.json b/packages/lint/package.json new file mode 100644 index 000000000..4ac11bc38 --- /dev/null +++ b/packages/lint/package.json @@ -0,0 +1,75 @@ +{ + "name": "@fedify/lint", + "version": "2.0.0", + "description": "Fedify linting rules and plugins", + "keywords": [ + "Fedify", + "ActivityPub", + "Fediverse", + "Lint", + "ESLint", + "ESLint Plugin" + ], + "author": { + "name": "Chanhaeng Lee", + "email": "2chanhaeng@gmail.com", + "url": "https://chomu.dev/" + }, + "homepage": "https://fedify.dev/", + "repository": { + "type": "git", + "url": "git+https://github.com/fedify-dev/fedify.git", + "directory": "packages/lint" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/fedify-dev/fedify/issues" + }, + "funding": [ + "https://opencollective.com/fedify", + "https://github.com/sponsors/dahlia" + ], + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.cts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist/", + "deno.json", + "package.json" + ], + "peerDependencies": { + "@fedify/fedify": "workspace:^", + "eslint": ">=9.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + }, + "dependencies": { + "@fxts/core": "catalog:", + "@typescript-eslint/parser": "^8.49.0", + "@typescript-eslint/utils": "^8.0.0" + }, + "devDependencies": { + "@types/eslint": "^9.0.0", + "@types/estree": "^1.0.8", + "eslint": "^9.0.0", + "tsdown": "catalog:", + "typescript": "catalog:" + }, + "scripts": { + "build": "tsdown", + "prepack": "tsdown", + "prepublish": "tsdown", + "test": "node --experimental-transform-types --test 'src/tests/**/*.test.ts'" + } +} diff --git a/packages/lint/src/index.ts b/packages/lint/src/index.ts new file mode 100644 index 000000000..5318bb62a --- /dev/null +++ b/packages/lint/src/index.ts @@ -0,0 +1,161 @@ +/** + * ESLint plugin for Fedify. + * Provides lint rules for validating Fedify federation code. + */ +import { fromEntries, keys, map, pipe } from "@fxts/core"; +import parser from "@typescript-eslint/parser"; +import type { ESLint, Rule } from "eslint"; +import metadata from "../deno.json" with { type: "json" }; +import { RULE_IDS } from "./lib/const.ts"; +import { + eslint as actorAssertionMethodRequired, +} from "./rules/actor-assertion-method-required.ts"; +import { + eslint as actorFeaturedPropertyMismatch, +} from "./rules/actor-featured-property-mismatch.ts"; +import { + eslint as actorFeaturedPropertyRequired, +} from "./rules/actor-featured-property-required.ts"; +import { + eslint as actorFeaturedTagsPropertyMismatch, +} from "./rules/actor-featured-tags-property-mismatch.ts"; +import { + eslint as actorFeaturedTagsPropertyRequired, +} from "./rules/actor-featured-tags-property-required.ts"; +import { + eslint as actorFollowersPropertyMismatch, +} from "./rules/actor-followers-property-mismatch.ts"; +import { + eslint as actorFollowersPropertyRequired, +} from "./rules/actor-followers-property-required.ts"; +import { + eslint as actorFollowingPropertyMismatch, +} from "./rules/actor-following-property-mismatch.ts"; +import { + eslint as actorFollowingPropertyRequired, +} from "./rules/actor-following-property-required.ts"; +import { eslint as actorIdMismatch } from "./rules/actor-id-mismatch.ts"; +import { eslint as actorIdRequired } from "./rules/actor-id-required.ts"; +import { + eslint as actorInboxPropertyMismatch, +} from "./rules/actor-inbox-property-mismatch.ts"; +import { + eslint as actorInboxPropertyRequired, +} from "./rules/actor-inbox-property-required.ts"; +import { + eslint as actorLikedPropertyMismatch, +} from "./rules/actor-liked-property-mismatch.ts"; +import { + eslint as actorLikedPropertyRequired, +} from "./rules/actor-liked-property-required.ts"; +import { + eslint as actorOutboxPropertyMismatch, +} from "./rules/actor-outbox-property-mismatch.ts"; +import { + eslint as actorOutboxPropertyRequired, +} from "./rules/actor-outbox-property-required.ts"; +import { + eslint as actorPublicKeyRequired, +} from "./rules/actor-public-key-required.ts"; +import { + eslint as actorSharedInboxPropertyMismatch, +} from "./rules/actor-shared-inbox-property-mismatch.ts"; +import { + eslint as actorSharedInboxPropertyRequired, +} from "./rules/actor-shared-inbox-property-required.ts"; +import { + eslint as collectionFiltering, +} from "./rules/collection-filtering-not-implemented.ts"; + +const rules: Record< + typeof RULE_IDS[keyof typeof RULE_IDS], + Rule.RuleModule +> = { + [RULE_IDS.actorIdMismatch]: actorIdMismatch, + [RULE_IDS.actorIdRequired]: actorIdRequired, + [RULE_IDS.actorFollowingPropertyRequired]: actorFollowingPropertyRequired, + [RULE_IDS.actorFollowingPropertyMismatch]: actorFollowingPropertyMismatch, + [RULE_IDS.actorFollowersPropertyRequired]: actorFollowersPropertyRequired, + [RULE_IDS.actorFollowersPropertyMismatch]: actorFollowersPropertyMismatch, + [RULE_IDS.actorOutboxPropertyRequired]: actorOutboxPropertyRequired, + [RULE_IDS.actorOutboxPropertyMismatch]: actorOutboxPropertyMismatch, + [RULE_IDS.actorLikedPropertyRequired]: actorLikedPropertyRequired, + [RULE_IDS.actorLikedPropertyMismatch]: actorLikedPropertyMismatch, + [RULE_IDS.actorFeaturedPropertyRequired]: actorFeaturedPropertyRequired, + [RULE_IDS.actorFeaturedPropertyMismatch]: actorFeaturedPropertyMismatch, + [RULE_IDS.actorFeaturedTagsPropertyRequired]: + actorFeaturedTagsPropertyRequired, + [RULE_IDS.actorFeaturedTagsPropertyMismatch]: + actorFeaturedTagsPropertyMismatch, + [RULE_IDS.actorInboxPropertyRequired]: actorInboxPropertyRequired, + [RULE_IDS.actorInboxPropertyMismatch]: actorInboxPropertyMismatch, + [RULE_IDS.actorSharedInboxPropertyRequired]: actorSharedInboxPropertyRequired, + [RULE_IDS.actorSharedInboxPropertyMismatch]: actorSharedInboxPropertyMismatch, + [RULE_IDS.actorPublicKeyRequired]: actorPublicKeyRequired, + [RULE_IDS.actorAssertionMethodRequired]: actorAssertionMethodRequired, + [RULE_IDS.collectionFilteringNotImplemented]: collectionFiltering, +}; + +const recommendedRuleIds: (keyof typeof rules)[] = [ + RULE_IDS.actorIdMismatch, + RULE_IDS.actorIdRequired, +]; + +/** + * Recommended configuration - enables all rules as warnings + */ +const recommendedRules = pipe( + rules, + keys, + map((key) => + [ + `@fedify/lint/${key}`, + recommendedRuleIds.includes(key) ? "error" : "warn", + ] as const + ), + fromEntries, +); + +/** + * Strict configuration - enables all rules as errors + */ +const strictRules = pipe( + rules, + keys, + map((key) => [`${metadata.name as "@fedify/lint"}/${key}`, "error"] as const), + fromEntries, +); + +export const plugin = { + meta: { + name: metadata.name, + version: metadata.version, + }, + rules, + configs: { + recommended: { + plugins: [metadata.name], + rules: recommendedRules, + }, + strict: { + plugins: [metadata.name], + rules: strictRules, + }, + }, +} as const satisfies ESLint.Plugin; + +const recommendedConfig = { + files: ["federation", "federation/*"].map((filename) => [ + filename + ".ts", + filename + ".tsx", + filename + ".js", + filename + ".jsx", + filename + ".mjs", + filename + ".cjs", + ]).flat(), + languageOptions: { parser }, + plugins: { [metadata.name]: plugin }, + rules: recommendedRules, +}; + +export default recommendedConfig; diff --git a/packages/lint/src/lib/const.ts b/packages/lint/src/lib/const.ts new file mode 100644 index 000000000..793e21520 --- /dev/null +++ b/packages/lint/src/lib/const.ts @@ -0,0 +1,138 @@ +import type { PropertyConfig } from "./types.ts"; + +export const FEDERATION_SETUP = ` +import { + createFederation, + Endpoints, + MemoryKvStore, + InProcessMessageQueue, +} from "@fedify/fedify"; + +const federation = createFederation({ + kv: new MemoryKvStore(), + queue: new InProcessMessageQueue(), +}); +` as const; + +/** + * Mapping of actor property names to their corresponding Context method names + * and dispatcher methods. + * Used by lint rules to validate property existence and correct method usage. + */ +export const properties = { + id: { + name: "id", + path: ["id"], + getter: "getActorUri", + setter: "setActorDispatcher", + requiresIdentifier: true, + }, + following: { + name: "following", + path: ["following"], + getter: "getFollowingUri", + setter: "setFollowingDispatcher", + requiresIdentifier: true, + }, + followers: { + name: "followers", + path: ["followers"], + getter: "getFollowersUri", + setter: "setFollowersDispatcher", + requiresIdentifier: true, + }, + outbox: { + name: "outbox", + path: ["outbox"], + getter: "getOutboxUri", + setter: "setOutboxDispatcher", + requiresIdentifier: true, + }, + inbox: { + name: "inbox", + path: ["inbox"], + getter: "getInboxUri", + setter: "setInboxListeners", + requiresIdentifier: true, + }, + liked: { + name: "liked", + path: ["liked"], + getter: "getLikedUri", + setter: "setLikedDispatcher", + requiresIdentifier: true, + }, + featured: { + name: "featured", + path: ["featured"], + getter: "getFeaturedUri", + setter: "setFeaturedDispatcher", + requiresIdentifier: true, + }, + featuredTags: { + name: "featuredTags", + path: ["featuredTags"], + getter: "getFeaturedTagsUri", + setter: "setFeaturedTagsDispatcher", + requiresIdentifier: true, + }, + sharedInbox: { + name: "sharedInbox", + path: ["endpoints", "sharedInbox"], + getter: "getInboxUri", + setter: "setInboxListeners", + requiresIdentifier: false, + nested: { + parent: "endpoints", + wrapper: "Endpoints", + }, + }, + publicKey: { + name: "publicKey", + path: ["publicKey"], + getter: "getActorKeyPairs", + setter: "setKeyPairsDispatcher", + requiresIdentifier: true, + isKeyProperty: true, + }, + assertionMethod: { + name: "assertionMethod", + path: ["assertionMethod"], + getter: "getActorKeyPairs", + setter: "setKeyPairsDispatcher", + requiresIdentifier: true, + isKeyProperty: true, + }, +} as const satisfies Record; + +/** + * Rule IDs for all Fedify lint rules + */ +export const RULE_IDS = { + // Required rules + actorIdRequired: "actor-id-required", + actorFollowingPropertyRequired: "actor-following-property-required", + actorFollowersPropertyRequired: "actor-followers-property-required", + actorOutboxPropertyRequired: "actor-outbox-property-required", + actorLikedPropertyRequired: "actor-liked-property-required", + actorFeaturedPropertyRequired: "actor-featured-property-required", + actorFeaturedTagsPropertyRequired: "actor-featured-tags-property-required", + actorInboxPropertyRequired: "actor-inbox-property-required", + actorSharedInboxPropertyRequired: "actor-shared-inbox-property-required", + actorPublicKeyRequired: "actor-public-key-required", + actorAssertionMethodRequired: "actor-assertion-method-required", + + // Mismatch rules + actorIdMismatch: "actor-id-mismatch", + actorFollowingPropertyMismatch: "actor-following-property-mismatch", + actorFollowersPropertyMismatch: "actor-followers-property-mismatch", + actorOutboxPropertyMismatch: "actor-outbox-property-mismatch", + actorLikedPropertyMismatch: "actor-liked-property-mismatch", + actorFeaturedPropertyMismatch: "actor-featured-property-mismatch", + actorFeaturedTagsPropertyMismatch: "actor-featured-tags-property-mismatch", + actorInboxPropertyMismatch: "actor-inbox-property-mismatch", + actorSharedInboxPropertyMismatch: "actor-shared-inbox-property-mismatch", + + // Collection rules + collectionFilteringNotImplemented: "collection-filtering-not-implemented", +} as const; diff --git a/packages/lint/src/lib/messages.ts b/packages/lint/src/lib/messages.ts new file mode 100644 index 000000000..af160860d --- /dev/null +++ b/packages/lint/src/lib/messages.ts @@ -0,0 +1,57 @@ +import type { MethodCallContext, PropertyConfig } from "./types.ts"; + +/** + * Generates error message for *-required rules. + * Used when a property is missing from the actor dispatcher return value. + * + * @param name - The property name (e.g., "id", "inbox", "following") + */ +export const actorPropertyRequired = ({ + setter, + path, + getter, + requiresIdentifier = true, +}: PropertyConfig): string => + `When \`${setter}\` is configured, the \`${ + path.join(".") + }\` property is recommended. Use \`${ + getExpectedCall({ + ctxName: "Context", + methodName: getter, + idName: "identifier", + path: path.join("."), + requiresIdentifier, + }) + }\` for the \`${path.join(".")}\` property URI.`; + +/** + * Generates error message for *-mismatch rules. + * Used when a property exists but uses the wrong context method. + * + * @param propertyName - The property name or path + * (e.g., "id", "endpoints.sharedInbox") + * @param expectedCall - The expected method call + * (e.g., "ctx.getActorUri(identifier)") + */ +export const actorPropertyMismatch = ( + context: MethodCallContext, +): string => + `Actor's \`${context.path}\` property must match \`${ + getExpectedCall(context) + }\`. \ +Ensure you're using the correct context method.`; + +const getExpectedCall = ( + { ctxName, methodName, requiresIdentifier, idName }: MethodCallContext, +): string => + requiresIdentifier + ? `${ctxName}.${methodName}(${idName})` + : `${ctxName}.${methodName}()`; + +/** + * Error message for collection filtering not implemented. + */ +export const COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR = + "Collection dispatcher should implement filtering to avoid large response " + + "payloads. Add a fourth parameter (filter) to handle filtering. " + + "See: https://fedify.dev/manual/collections#filtering-by-server"; diff --git a/packages/lint/src/lib/mismatch.ts b/packages/lint/src/lib/mismatch.ts new file mode 100644 index 000000000..0833dbc74 --- /dev/null +++ b/packages/lint/src/lib/mismatch.ts @@ -0,0 +1,187 @@ +import { isEmpty, negate, some } from "@fxts/core"; +import type { Rule } from "eslint"; +import { actorPropertyMismatch } from "./messages.ts"; +import { + allOf, + hasMemberExpressionCallee, + isFunction, + isNodeName, + isNodeType, + isSetActorDispatcherCall, +} from "./pred.ts"; +import { + createPropertyChecker, + createPropertySearcher, +} from "./property-checker.ts"; +import { trackFederationVariables } from "./tracker.ts"; +import type { + CallExpression, + Expression, + FunctionNode, + Identifier, + MethodCallContext, + Node, + Parameter, + PrivateIdentifier, + PropertyConfig, + SpreadElement, +} from "./types.ts"; + +const isIdentifierWithName = (name: T) => +( + node: Expression | SpreadElement | PrivateIdentifier, +): node is Identifier & { "name": T } => + allOf(isNodeType("Identifier"), isNodeName(name))(node); + +/** + * Checks if a node is a CallExpression calling the expected context method. + */ +const isExpectedMethodCall = ( + { + ctxName, + idName, + methodName, + requiresIdentifier, + }: MethodCallContext, +) => +(node: Node): boolean => { + if ( + !isNodeType("CallExpression")(node) || + !hasMemberExpressionCallee(node) || + !isIdentifierWithName(ctxName)(node.callee.object) || + !isIdentifierWithName(methodName)(node.callee.property) + ) return false; + + if (!requiresIdentifier) return isEmpty(node.arguments); + return allOf( + negate(isEmpty), + some(isIdentifierWithName(idName)), + )(node.arguments); +}; + +/** + * Extracts parameter names from a function. + */ +const extractParams = ( + fn: FunctionNode, +): [string | null, string | null] => { + const params = fn.params; + if (params.length < 2) return [null, null]; + + return params.slice(0, 2).map(getNameIfIdentifier) as [ + string | null, + string | null, + ]; +}; + +const getNameIfIdentifier = (node: Parameter): string | null => + node?.type === "Identifier" ? node.name : null; + +function createMismatchRule( + config: PropertyConfig, + describe: ( + methodCallContext: MethodCallContext, + ) => Context extends Deno.lint.RuleContext ? { + message: string; + } + : { + messageId: string; + data: { message: string }; + }, +) { + return (context: Context) => { + const tracker = trackFederationVariables(); + + return { + VariableDeclarator: tracker.VariableDeclarator, + + CallExpression(node: CallExpression) { + if ( + !isSetActorDispatcherCall(node) || + !hasMemberExpressionCallee(node) || + !tracker.isFederationObject(node.callee.object) + ) return; + + const dispatcherArg = node.arguments[1]; + if (!isFunction(dispatcherArg)) return; + + const [ctxName, idName] = extractParams(dispatcherArg); + if (!ctxName || !idName) return; + + const methodCallContext: MethodCallContext = { + path: config.path.join("."), + ctxName, + idName, + methodName: config.getter, + requiresIdentifier: config.requiresIdentifier, + }; + + const existenceChecker = createPropertyChecker(Boolean)(config.path); + const hasProperty = createPropertySearcher(existenceChecker)( + dispatcherArg.body, + ); + + // If property doesn't exist, don't report (that's for *-required rules) + if (!hasProperty) return; + + // Property exists, now check if the value is correct + const propertyChecker = createPropertyChecker( + isExpectedMethodCall(methodCallContext), + )(config.path); + const propertySearcher = createPropertySearcher( + propertyChecker, + ); + + if (!propertySearcher(dispatcherArg.body)) { + (context as { report: (arg: unknown) => void }).report({ + node: dispatcherArg, + ...describe(methodCallContext), + }); + } + }, + }; + }; +} + +/** + * Creates a lint rule that checks if a property uses + * the correct context method. + * + * @param config Property configuration containing name, getter, setter, and + * nested info + * @returns A Deno lint rule + */ +export const createMismatchRuleDeno = ( + config: PropertyConfig, +): Deno.lint.Rule => ({ + create: createMismatchRule( + config, + (context) => ({ + message: actorPropertyMismatch(context), + }), + ), +}); + +export const createMismatchRuleEslint = ( + config: PropertyConfig, +): Rule.RuleModule => ({ + meta: { + type: "problem", + docs: { + description: `Ensure actor's ${ + config.path.join(".") + } property uses correct context method`, + }, + schema: [], + messages: { + mismatch: "{{ message }}", + }, + }, + create: createMismatchRule( + config, + (context) => ({ + messageId: "mismatch", + data: { message: actorPropertyMismatch(context) }, + }), + ), +}); diff --git a/packages/lint/src/lib/pred.ts b/packages/lint/src/lib/pred.ts new file mode 100644 index 000000000..5ea3a97d6 --- /dev/null +++ b/packages/lint/src/lib/pred.ts @@ -0,0 +1,142 @@ +import { isObject, pipe, prop } from "@fxts/core"; +import type { + CallExpression, + CallMemberExpression, + CallMemberExpressionWithIdentified, + Expression, + FunctionNode, + Node, + SpreadElement, +} from "./types.ts"; +import { eq } from "./utils.ts"; + +interface Predicate { + (value: unknown): value is T; +} +/** + * Combines multiple predicates with AND logic. + */ +export function allOf( + ...refinements: (((value: T) => boolean) | Predicate)[] +): (v: T) => v is S; +export function allOf( + ...predicates: ((value: T) => boolean)[] +): (value: T) => boolean; +export function allOf( + ...predicates: Array<(value: T) => boolean> +): (value: T) => boolean { + return (value: T): boolean => + predicates.every((predicate) => predicate(value)); +} + +export const anyOf = + (...predicates: ((value: T) => boolean)[]) => (value: T): boolean => + predicates.some((predicate) => predicate(value)); + +export const isNode = (obj: unknown): obj is Node => + isObject(obj) && "type" in obj; + +/** + * Checks if a node is of a specific type. + */ +export const isNodeType = + (type: T) => (node: Node): node is { "type": T } & Node => + pipe( + node, + prop("type"), + eq(type), + ) as boolean; + +/** + * Checks if a node is of a specific name. + */ +export const isNodeName = + (name: T) => (node: N): node is N & { "name": T } => + pipe( + node as { "name": string }, + prop("name"), + eq(name), + ) as boolean; + +/** + * Checks if a node's callee is a MemberExpression. + */ +export const hasMemberExpressionCallee = ( + node: CallExpression, +): node is CallMemberExpression => node.callee.type === "MemberExpression"; + +/** + * Checks if a node's callee property is an Identifier. + */ +export const hasIdentifierProperty = ( + node: CallExpression, +): node is CallMemberExpressionWithIdentified => + "callee" in node && + "property" in node.callee && + "type" in node.callee.property && + node.callee.property.type === "Identifier"; + +/** + * Checks if a node's callee property name matches the given method name. + */ +export const hasMethodName = + (methodName: T) => + (node: CallExpression): node is CallExpression & { + callee: { property: { name: T } }; + } => + "callee" in node && + "property" in node.callee && + "name" in node.callee.property && + node.callee.property.name === methodName; + +/** + * Checks if a CallExpression has minimum required arguments. + */ +export const hasMinArguments = + (min: number) => + (node: T): node is Extract => node.arguments.length >= min; + +/** + * Checks if an expression is a function (arrow or regular). + */ +export const isFunction = ( + expr: + | Expression + | SpreadElement, +): expr is FunctionNode => + anyOf( + isNodeType("ArrowFunctionExpression"), + isNodeType("FunctionExpression"), + )(expr); + +/** + * Checks if a CallExpression is a setActorDispatcher call with + * proper structure. + */ +export const isSetActorDispatcherCall = ( + node: CallExpression, +): node is CallMemberExpressionWithIdentified & { + callee: { property: Deno.lint.Identifier & { name: "setActorDispatcher" } }; +} => + allOf( + hasMemberExpressionCallee, + hasIdentifierProperty, + hasMethodName("setActorDispatcher"), + hasMinArguments(2), + )(node); + +/** + * Checks if an object has a specific property key. + */ +export const hasProp = + (key: K) => + (obj: T): obj is Extract> => + Object.prototype.hasOwnProperty.call(obj, key); + +/** + * Checks if a function has at least n parameters. + */ +export const hasMinParams = (min: number) => (fn: FunctionNode): boolean => + fn.params.length >= min; diff --git a/packages/lint/src/lib/property-checker.ts b/packages/lint/src/lib/property-checker.ts new file mode 100644 index 000000000..4a17e2c0c --- /dev/null +++ b/packages/lint/src/lib/property-checker.ts @@ -0,0 +1,290 @@ +import { + always, + every, + head, + isEmpty, + isObject, + pipe, + pipeLazy, + prop, + toArray, + unless, + when, +} from "@fxts/core"; +import { allOf, isNodeType } from "./pred.ts"; +import type { + AssignmentPattern, + BlockStatement, + ConditionalExpression, + Expression, + NewExpression, + Node, + ObjectExpression, + Property, + PropertyChecker, + ReturnStatement, + SpreadElement, + Statement, + WithIdentifierKey, +} from "./types.ts"; +import { cases, eq } from "./utils.ts"; + +/** + * Checks if a node has a key with a specific name. + */ +const hasKeyName = + (propertyName: T) => + (node: Property): node is Property & WithIdentifierKey => + pipe( + node, + prop("key"), + allOf( + isNodeType("Identifier"), + pipeLazy(prop("name"), eq(propertyName)) as (node: Node) => boolean, + ), + ) as boolean; + +/** + * Checks if a node is a Property with an Identifier key of a specific name. + */ +export const isPropertyWithName = (propertyName: T) => +( + node: Property | SpreadElement, +): node is Property & WithIdentifierKey => + allOf( + isNodeType("Property"), + hasKeyName(propertyName), + )(node as Expression & Property); + +/** + * Creates a predicate function that checks if a nested property exists. + * @param path Array of property names forming the path + * (e.g., ["endpoints", "sharedInbox"]) + * @returns A predicate function that checks if the nested property exists + */ +export function createPropertyChecker( + checker: ( + node: + | Expression + | AssignmentPattern, + ) => boolean, +): (path: readonly string[]) => PropertyChecker { + const inner = + ([first, ...rest]: readonly string[]): PropertyChecker => (node) => { + if (!isPropertyWithName(first)(node)) return false; + + // Base case: last property in path + if (isEmpty(rest)) { + return checker(node.value as Expression | AssignmentPattern); + } + + // Handle NewExpression: endpoints: new Endpoints({ sharedInbox: ... }) + if (isNodeType("NewExpression")(node.value)) { + if (node.value.arguments.length === 0) return false; + const firstArg = node.value.arguments[0]; + if (!isNodeType("ObjectExpression")(firstArg)) return false; + return firstArg.properties.some(inner(rest)); + } + + return false; + }; + return inner; +} + +/** + * Checks if an ObjectExpression node contains a property. + * @param propertyChecker The predicate function to check properties + * @returns A function that checks the ObjectExpression + */ +const checkObjectExpression = + (propertyChecker: PropertyChecker) => (obj: ObjectExpression): boolean => + obj.properties.some(propertyChecker); + +/** + * Checks if a ConditionalExpression (ternary operator) has the property in + * both branches. + * @param propertyChecker The predicate function to check properties + * @returns A function that checks the ConditionalExpression + */ +const checkConditionalExpression = + (propertyChecker: PropertyChecker) => + (node: ConditionalExpression): boolean => + [node.consequent, node.alternate].every(checkBranchWith(propertyChecker)); + +// Check if both branches have the property +const checkBranchWith = + (propertyChecker: PropertyChecker) => (branch: Expression): boolean => + pipe( + branch, + cases( + isNodeType("ConditionalExpression"), + checkConditionalExpression(propertyChecker), + pipeLazy( + extractObjectExpression, + cases( + isObject, + checkObjectExpression(propertyChecker), + always(false) as (_: null) => boolean, + ), + ), + ) as (node: Expression) => boolean, + ); + +/** + * Extracts the first argument if it's an ObjectExpression. + */ +const extractFirstObjectExpression = (node: NewExpression): + | ObjectExpression + | null => + pipe( + node, + prop("arguments"), + head, + unless( + isNodeType("ObjectExpression"), + always(null), + ) as () => ObjectExpression | null, + ); + +/** + * Extracts ObjectExpression from NewExpression. + */ +const extractObjectExpression: (arg: Expression) => ObjectExpression | null = + cases( + isNodeType("NewExpression"), + extractFirstObjectExpression, + always(null), + ) as (arg: Expression) => ObjectExpression | null; + +/** + * Checks if a ReturnStatement node contains a property. + * @param propertyChecker The predicate function to check properties + * @returns A function that checks the ReturnStatement + */ +const checkReturnStatement = + (propertyChecker: PropertyChecker) => (node: ReturnStatement) => + pipe( + node, + prop("argument"), + cases( + isObject, + checkBranchWith(propertyChecker), + always(false), + ), + ); + +/** + * Creates a function that recursively checks for a property in an AST node. + * @param propertyChecker The predicate function to check properties + * @returns A recursive function that checks the AST node + */ +export const createPropertySearcher = (propertyChecker: PropertyChecker) => { + return ( + node: Expression | BlockStatement | Statement, + ): node is + | ReturnStatement + | BlockStatement + | NewExpression => { + switch (node.type) { + case "ReturnStatement": + return checkReturnStatement(propertyChecker)(node); + + case "BlockStatement": + return checkAllReturnPaths(propertyChecker)(node); + + case "NewExpression": + return pipe( + node, + extractFirstObjectExpression, + when(isObject, checkObjectExpression(propertyChecker)), + Boolean, + ); + + default: + return false; + } + }; +}; + +const checkAllReturnPaths = (propertyChecker: PropertyChecker) => +( + node: Expression | BlockStatement | Statement, +): boolean => + pipe( + node, + collectReturnPaths, + cases( + isEmpty, + always(false), + every(checkReturnStatement(propertyChecker)), + ), + ); + +/** + * Collects all return statements from a node, traversing control flow. + * This handles if/else branches, loops, etc. + */ +const collectReturnPaths = ( + node: Expression | BlockStatement | Statement, +): ReturnStatement[] => + pipe( + node, + flatten, + toArray, + ); + +function* flatten(node: Node): Generator { + switch (node.type) { + case "ReturnStatement": + yield node; + return; + + case "IfStatement": + // Collect returns from both branches + if (node.consequent) yield* flatten(node.consequent); + if (node.alternate) yield* flatten(node.alternate); + return; + + case "BlockStatement": + for (const statement of node.body) { + yield* flatten(statement); + } + return; + + case "SwitchStatement": + for (const switchCase of node.cases) { + for (const statement of switchCase.consequent) { + yield* flatten(statement); + } + } + return; + + case "TryStatement": + yield* flatten(node.block); + if (node.handler) yield* flatten(node.handler.body); + if (node.finalizer) yield* flatten(node.finalizer); + return; + + case "WhileStatement": + case "DoWhileStatement": + case "ForStatement": + case "ForInStatement": + case "ForOfStatement": + yield* flatten(node.body); + return; + + case "LabeledStatement": + yield* flatten(node.body); + return; + + case "WithStatement": + yield* flatten(node.body); + return; + + default: + // For other node types (expressions, declarations, etc.), + // we don't traverse deeper to avoid infinite recursion + // from circular references like `parent` + return; + } +} diff --git a/packages/lint/src/lib/required.ts b/packages/lint/src/lib/required.ts new file mode 100644 index 000000000..8d928de87 --- /dev/null +++ b/packages/lint/src/lib/required.ts @@ -0,0 +1,154 @@ +import type { Rule } from "eslint"; +import { actorPropertyRequired } from "./messages.ts"; +import { + allOf, + hasIdentifierProperty, + hasMemberExpressionCallee, + hasMethodName, + isFunction, + isSetActorDispatcherCall, +} from "./pred.ts"; +import { + createPropertyChecker, + createPropertySearcher, +} from "./property-checker.ts"; +import { trackFederationVariables } from "./tracker.ts"; +import type { + CallExpression, + CallMemberExpressionWithIdentified, + FunctionNode, + PropertyConfig, +} from "./types.ts"; + +/** + * Checks if a CallExpression is a specific dispatcher method call. + */ +const isDispatcherMethodCall = + (methodName: string) => + (node: CallExpression): node is CallMemberExpressionWithIdentified => + allOf( + hasMemberExpressionCallee, + hasIdentifierProperty, + hasMethodName(methodName), + )(node); + +/** + * Tracks dispatcher method calls on federation objects. + */ +const createDispatcherTracker = ( + dispatcherMethod: string, + federationTracker: ReturnType, +) => { + let dispatcherConfigured = false; + + return { + isDispatcherConfigured: () => dispatcherConfigured, + checkDispatcherCall: ( + node: CallExpression, + ) => { + if ( + isDispatcherMethodCall(dispatcherMethod)(node) && + federationTracker.isFederationObject(node.callee.object) + ) { + dispatcherConfigured = true; + } + }, + }; +}; + +function createRequiredRule( + config: PropertyConfig, + description: Context extends Deno.lint.RuleContext ? { + message: string; + } + : { + messageId: string; + data: { message: string }; + }, +) { + return (context: Context) => { + const federationTracker = trackFederationVariables(); + const dispatcherTracker = createDispatcherTracker( + config.setter, + federationTracker, + ); + const actorDispatchers: FunctionNode[] = []; + + const propertyChecker = createPropertyChecker(Boolean)( + config.path, + ); + const propertySearcher = createPropertySearcher(propertyChecker); + + return { + VariableDeclarator: federationTracker.VariableDeclarator, + + CallExpression(node: CallExpression) { + dispatcherTracker.checkDispatcherCall(node); + + if (!isSetActorDispatcherCall(node)) return; + if (!federationTracker.isFederationObject(node.callee.object)) return; + + const dispatcher = node.arguments[1]; + if (isFunction(dispatcher)) { + actorDispatchers.push(dispatcher); + } + }, + + "Program:exit"() { + if (!dispatcherTracker.isDispatcherConfigured()) return; + + for (const dispatcher of actorDispatchers) { + if (!propertySearcher(dispatcher.body)) { + (context as { report: (arg: unknown) => void }).report({ + node: dispatcher, + ...description, + }); + } + } + }, + }; + }; +} + +/** + * Creates a required rule with the given property configuration. + */ +export function createRequiredRuleDeno( + config: PropertyConfig, +): Deno.lint.Rule { + return { + create: createRequiredRule( + config, + { message: actorPropertyRequired(config) }, + ), + }; +} + +/** + * Creates a required ESLint rule with the given property configuration. + */ +export function createRequiredRuleEslint( + config: PropertyConfig, +): Rule.RuleModule { + return { + meta: { + type: "suggestion", + docs: { + description: `Ensure actor dispatcher returns ${ + config.path.join(".") + } property.`, + }, + schema: [], + messages: { + required: "{{ message }}", + }, + }, + create: createRequiredRule( + config, + { + messageId: "required", + data: { message: actorPropertyRequired(config) }, + }, + ), + }; +} diff --git a/packages/lint/src/lib/test-templates.ts b/packages/lint/src/lib/test-templates.ts new file mode 100644 index 000000000..bd18454ff --- /dev/null +++ b/packages/lint/src/lib/test-templates.ts @@ -0,0 +1,1666 @@ +import { consume, entries, map, pipe } from "@fxts/core"; +import type { Rule } from "eslint"; +import { test } from "node:test"; +import { properties } from "./const.ts"; +import { actorPropertyMismatch, actorPropertyRequired } from "./messages.ts"; +import lintTest from "./test.ts"; +import type { MethodCallContext, PropertyConfig } from "./types.ts"; + +type PropertyKey = keyof typeof properties; + +interface TestConfig { + rule: { + deno: Deno.lint.Rule; + eslint: Rule.RuleModule; + }; + ruleName: string; +} + +type TestEntry = [() => void, boolean]; +type TestSuite = Record; + +const ID_PROP = "id: ctx.getActorUri(identifier),"; + +/** + * Runs all tests in a test suite with proper formatting + */ +export const runTests = (ruleName: string, tests: TestSuite) => + pipe( + tests, + entries, + map(([testName, [testFn, succeed]]) => { + test( + `${ruleName}: ${succeed ? "✅ Good" : "❌ Bad"} - ${testName}`, + testFn, + ); + }), + consume, + ); + +const createActorDispatcherCode = (content: string) => ` +federation.setActorDispatcher( + "/users/{identifier}", + async (ctx, identifier) => { + ${content} +}); +`; + +const createChainedDispatcherCode = ( + content: string, + dispatcherMethod: string, +) => ` + federation + .setActorDispatcher( + "/users/{identifier}", + async (ctx, identifier) => { + ${content} + }) + .${dispatcherMethod}(async (ctx, identifier) => []); +`; + +const createDispatcherCode = ( + content: string, + dispatcherMethod: string, + isBefore?: boolean | undefined, +) => { + const dispatcher = + `federation.${dispatcherMethod}(async (ctx, identifier) => []);`; + const actor = createActorDispatcherCode(content); + return isBefore ? `${dispatcher}\n${actor}` : `${actor}\n${dispatcher}`; +}; + +/** + * Generates the method call code for a property. + * @param getter The getter method name + * @param requiresIdentifier Whether the method requires an identifier parameter + * @param ctxName The context variable name (default: "ctx") + * @param idName The identifier variable name (default: "identifier") + */ +const createMethodCall = ( + getter: string, + requiresIdentifier: boolean, + ctxName = "ctx", + idName = "identifier", +): string => { + return requiresIdentifier + ? `${ctxName}.${getter}(${idName})` + : `${ctxName}.${getter}()`; +}; + +/** + * Generates property assignment code for a given property configuration. + * Handles both simple properties and nested properties with wrappers. + * + * @example + * // Simple property: id: ctx.getActorUri(identifier) + * // Nested property: + * // endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri() }), + */ +const createPropertyAssignment = ( + prop: PropertyConfig, + options: { + getter?: string; + ctxName?: string; + idName?: string; + } = {}, +): string => { + const getter = options.getter ?? prop.getter; + const ctxName = options.ctxName ?? "ctx"; + const idName = options.idName ?? "identifier"; + const requiresIdentifier = prop.requiresIdentifier ?? true; + + const methodCall = createMethodCall( + getter, + requiresIdentifier, + ctxName, + idName, + ); + + if (prop.nested) { + return `${prop.nested.parent}: \ + new ${prop.nested.wrapper}({ ${prop.name}: ${methodCall} }),`; + } + return `${prop.name}: ${methodCall},`; +}; + +/** + * Creates a MethodCallContext for error message generation. + */ +const createMethodCallContext = ( + prop: PropertyConfig, + ctxName = "ctx", + idName = "identifier", +): MethodCallContext => ({ + path: prop.path.join("."), + ctxName, + idName, + methodName: prop.getter, + requiresIdentifier: prop.requiresIdentifier ?? true, +}); + +const createTestCode = ( + propertyKey: PropertyKey, + includeProperty: boolean, +) => { + const prop = properties[propertyKey]; + + return `return new Person({ + ${ID_PROP} + ${includeProperty ? createPropertyAssignment(prop) : ""} + name: "John Doe", + });`; +}; + +// ============================================================================= +// Required Rule Tests (Standard Properties) +// ============================================================================= + +/** + * Creates required rule tests for standard properties that use a dispatcher + * (following, followers, outbox, liked, featured, featuredTags) + */ +export const createRequiredDispatcherRuleTests = + requiredDispatcherRuleTestsFactory(createDispatcherCode, false); +export const createKeyRequiredDispatcherRuleTests = + requiredDispatcherRuleTestsFactory(createChainedDispatcherCode, true); + +function requiredDispatcherRuleTestsFactory( + createDispatcherCode: ( + content: string, + dispatcherMethod: string, + isBefore?: boolean, + ) => string, + isKeyRequired: false, +): ( + propertyKey: PropertyKey, + config: TestConfig, +) => TestSuite; +function requiredDispatcherRuleTestsFactory( + createDispatcherCode: (content: string, dispatcherMethod: string) => string, + isKeyRequired: true, +): ( + propertyKey: PropertyKey, + config: TestConfig, +) => TestSuite; +function requiredDispatcherRuleTestsFactory( + createDispatcherCode: ( + content: string, + dispatcherMethod: string, + isBefore?: boolean, + ) => string, + isKeyRequired: boolean, +): ( + propertyKey: PropertyKey, + config: TestConfig, +) => TestSuite { + return function ( + propertyKey: PropertyKey, + config: TestConfig, + ): TestSuite { + const { rule, ruleName } = config; + const prop = properties[propertyKey]; + const expectedError = actorPropertyRequired(prop); + + return { + // ✅ Good - non-Federation object + "non-federation object": [ + lintTest({ + code: createDispatcherCode( + createTestCode(propertyKey, false), + prop.setter, + ), + rule, + ruleName, + federationSetup: ` + const federation = { setActorDispatcher: () => {} }; + `, + }), + true, + ], + + // ✅ Good - dispatcher NOT configured + "dispatcher not configured": [ + lintTest({ + code: createActorDispatcherCode(createTestCode(propertyKey, false)), + rule, + ruleName, + }), + true, + ], + ...(isKeyRequired ? {} : { + // ✅ Good - dispatcher configured BEFORE + "dispatcher before separate with property": [ + lintTest({ + code: createDispatcherCode( + createTestCode(propertyKey, true), + prop.setter, + true, + ), + rule, + ruleName, + }), + true, + ], + + // ✅ Good - dispatcher configured AFTER + "dispatcher after separate with property": [ + lintTest({ + code: createDispatcherCode( + createTestCode(propertyKey, true), + prop.setter, + false, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ Bad - dispatcher before, property missing + "dispatcher before separate property missing": [ + lintTest({ + code: createDispatcherCode( + createTestCode(propertyKey, false), + prop.setter, + true, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Bad - dispatcher after, property missing + "dispatcher after separate property missing": [ + lintTest({ + code: createDispatcherCode( + createTestCode(propertyKey, false), + prop.setter, + false, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + }), + }; + }; +} + +/** + * Creates required rule tests for actor id property (no dispatcher needed) + */ +export function createIdRequiredRuleTests(config: TestConfig): TestSuite { + const { rule, ruleName } = config; + const expectedError = actorPropertyRequired(properties.id); + + return { + // ✅ Good - non-Federation object + "non-federation object": [ + lintTest({ + code: createActorDispatcherCode( + `return new Person({ name: "John Doe" });`, + ), + rule, + ruleName, + federationSetup: ` + const federation = { setActorDispatcher: () => {} }; + `, + }), + true, + ], + + // ✅ Good - with id property (any value) + "with id property any value": [ + lintTest({ + code: createActorDispatcherCode(`return new Person({ + id: "https://example.com/users/123", + name: "John Doe", + });`), + rule, + ruleName, + }), + true, + ], + + // ✅ Good - with id property using ctx.getActorUri() + "with id property using getActorUri": [ + lintTest({ + code: createActorDispatcherCode(`return new Person({ + ${ID_PROP} + name: "John Doe", + });`), + rule, + ruleName, + }), + true, + ], + + // ✅ Good - BlockStatement with id + "block statement with id": [ + lintTest({ + code: createActorDispatcherCode(`const name = "John Doe"; + return new Person({ + ${ID_PROP} + name, + });`), + rule, + ruleName, + }), + true, + ], + + // ❌ Bad - without id property + "without id property": [ + lintTest({ + code: createActorDispatcherCode( + `return new Person({ name: "John Doe" });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Bad - returning empty object + "returning empty object": [ + lintTest({ + code: createActorDispatcherCode(`return new Person({});`), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ✅ Good - multiple properties including id + "multiple properties including id": [ + lintTest({ + code: createActorDispatcherCode(`return new Person({ + ${ID_PROP} + name: "John Doe", + inbox: ctx.getInboxUri(identifier), + outbox: ctx.getOutboxUri(identifier), + });`), + rule, + ruleName, + }), + true, + ], + + // ❌ Bad - variable assignment without id + "variable assignment without id": [ + lintTest({ + code: createActorDispatcherCode( + `const actor = new Person({ name: "John Doe" }); + return actor;`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + }; +} + +// ============================================================================= +// Mismatch Rule Tests +// ============================================================================= + +/** + * Creates mismatch rule tests for standard properties + */ +export function createMismatchRuleTests( + propertyKey: PropertyKey, + config: TestConfig, +): TestSuite { + const { rule, ruleName } = config; + const prop = properties[propertyKey]; + + // Build the expected method call context + const expectedError = actorPropertyMismatch(createMethodCallContext(prop)); + + // Find a wrong getter for testing + const wrongGetters = Object.values(properties) + .filter((p) => p.getter !== prop.getter) + .map((p) => p.getter); + const wrongGetter = wrongGetters[0] || "getWrongUri"; + const wrongSetter = Object.values(properties) + .filter((p) => p.setter !== prop.setter) + .map((p) => p.setter)[0] || "setWrongDispatcher"; + + const createLocalPropertyCode = (getter: string) => + createPropertyAssignment(prop, { getter }); + + const createActorCode = (getter: string) => + `return new Person({ + ${ID_PROP} + ${createLocalPropertyCode(getter)} + name: "John Doe", + });`; + + return { + // ✅ Good - non-Federation object + "non-federation object": [ + lintTest({ + code: createDispatcherCode(createActorCode(wrongGetter), wrongSetter), + rule, + ruleName, + federationSetup: ` + const federation = { setActorDispatcher: () => {} }; + `, + }), + true, + ], + + // ✅ Good - correct getter used + "correct getter used": [ + lintTest({ + code: createDispatcherCode(createActorCode(prop.getter), prop.setter), + rule, + ruleName, + }), + true, + ], + + // ❌ Bad - wrong getter used + "wrong getter used": [ + lintTest({ + code: createDispatcherCode(createActorCode(wrongGetter), wrongSetter), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Bad - wrong identifier + "wrong context": [ + lintTest({ + code: createActorDispatcherCode(`return new Person({ + ${ID_PROP} + ${createPropertyAssignment(prop, { ctxName: "wrongContext" })} + name: "John Doe", + });`), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ✅ Good - property not present (no error) + "property not present": [ + lintTest({ + code: createActorDispatcherCode(`return new Person({ + ${ID_PROP} + name: "John Doe", + });`), + rule, + ruleName, + }), + true, + ], + }; +} + +/** + * Creates mismatch rule tests for id property + */ +export function createIdMismatchRuleTests(config: TestConfig): TestSuite { + const { rule, ruleName } = config; + const expectedError = actorPropertyMismatch( + createMethodCallContext(properties.id), + ); + + return { + // ✅ Good - non-Federation object + "non-federation object": [ + lintTest({ + code: createActorDispatcherCode( + `return new Person({ id: ctx.getFollowingUri(identifier) });`, + ), + rule, + ruleName, + federationSetup: ` + const federation = { setActorDispatcher: () => {} }; + `, + }), + true, + ], + + // ✅ Good - correct getter used + "correct getter used": [ + lintTest({ + code: createActorDispatcherCode( + `return new Person({ ${ID_PROP} });`, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ Bad - literal string id + "literal string id": [ + lintTest({ + code: createActorDispatcherCode( + `return new Person({ id: "https://example.com/users/123" });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Bad - new URL as id + "new URL as id": [ + lintTest({ + code: createActorDispatcherCode( + `return new Person({ id: new URL("https://example.com/users/123") });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Bad - wrong getter used + "wrong getter used": [ + lintTest({ + code: createActorDispatcherCode( + `return new Person({ id: ctx.getFollowingUri(identifier) });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Bad - wrong identifier + "wrong context": [ + lintTest({ + code: createActorDispatcherCode( + `return new Person({ id: wrongContext.getActorUri(identifier) });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + }; +} + +// ============================================================================= +// Edge Case Tests +// ============================================================================= + +/** + * Creates common edge case tests for required rules + */ +export const createRequiredEdgeCaseTests = requiredEdgeCaseTestsFactory( + createDispatcherCode, +); + +/** + * Creates edge case tests for key required rules (publicKey, assertionMethod) + */ +export const createKeyRequiredEdgeCaseTests = requiredEdgeCaseTestsFactory( + createChainedDispatcherCode, +); + +function requiredEdgeCaseTestsFactory( + createDispatcherCode: (content: string, dispatcherMethod: string) => string, +) { + return function ( + propertyKey: PropertyKey, + config: TestConfig, + ): TestSuite { + const { rule, ruleName } = config; + const prop = properties[propertyKey]; + const setter = prop.setter; + const expectedError = actorPropertyRequired(prop); + const propCode = createPropertyAssignment(prop); + + return { + // ✅ Ternary with property in both branches + "ternary with property in both branches": [ + lintTest({ + code: createDispatcherCode( + `return condition + ? new Person({ ${ID_PROP} ${propCode} name: "A" }) + : new Person({ ${ID_PROP} ${propCode} name: "B" });`, + setter, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ Ternary missing property in consequent + "ternary missing property in consequent": [ + lintTest({ + code: createDispatcherCode( + `return condition + ? new Person({ ${ID_PROP} name: "A" }) + : new Person({ ${ID_PROP} ${propCode} name: "B" });`, + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Ternary missing property in alternate + "ternary missing property in alternate": [ + lintTest({ + code: createDispatcherCode( + `return condition + ? new Person({ ${ID_PROP} ${propCode} name: "A" }) + : new Person({ ${ID_PROP} name: "B" });`, + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Ternary missing property in both + "ternary missing property in both branches": [ + lintTest({ + code: createDispatcherCode( + `return condition + ? new Person({ ${ID_PROP} name: "A" }) + : new Person({ ${ID_PROP} name: "B" });`, + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ✅ Nested ternary with property + "nested ternary with property": [ + lintTest({ + code: createDispatcherCode( + `return condition1 + ? (condition2 + ? new Person({ ${ID_PROP} ${propCode} name: "A" }) + : new Person({ ${ID_PROP} ${propCode} name: "B" })) + : new Person({ ${ID_PROP} ${propCode} name: "C" });`, + setter, + ), + rule, + ruleName, + }), + true, + ], + + // ✅ If/else with property in both branches + "if else with property in both branches": [ + lintTest({ + code: createDispatcherCode( + `\ +if (condition) { + return new Person({ ${ID_PROP} ${propCode} name: "A" }); +} else { + return new Person({ ${ID_PROP} ${propCode} name: "B" }); +}`, + setter, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ If/else missing property in if block + "if else missing property in if block": [ + lintTest({ + code: createDispatcherCode( + `\ +if (condition) { + return new Person({ ${ID_PROP} name: "A" }); +} else { + return new Person({ ${ID_PROP} ${propCode} name: "B" }); +}`, + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ If/else missing property in else block + "if else missing property in else block": [ + lintTest({ + code: createDispatcherCode( + `\ +if (condition) { + return new Person({ ${ID_PROP} ${propCode} name: "A" }); +} else { + return new Person({ ${ID_PROP} name: "B" }); +}`, + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ If/else missing property in both blocks + "if else missing property in both blocks": [ + lintTest({ + code: createDispatcherCode( + `\ +if (condition) { + return new Person({ ${ID_PROP} name: "A" }); +} else { + return new Person({ ${ID_PROP} name: "B" }); +}`, + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ✅ Nested if with property + "nested if with property": [ + lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + if (condition2) { + return new Person({ ${ID_PROP} ${propCode} name: "A" }); + } else { + return new Person({ ${ID_PROP} ${propCode} name: "B" }); + } +} else { + return new Person({ ${ID_PROP} ${propCode} name: "C" }); +}`, + setter, + ), + rule, + ruleName, + }), + true, + ], + + // ✅ If/else if/else with property in all branches + "if else if else with property in all branches": [ + lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + return new Person({ ${ID_PROP} ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ ${ID_PROP} ${propCode} name: "B" }); +} else { + return new Person({ ${ID_PROP} ${propCode} name: "C" }); +}`, + setter, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ If/else if/else missing property in else if + "if else if else missing property in else if": [ + lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + return new Person({ ${ID_PROP} ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ ${ID_PROP} name: "B" }); +} else { + return new Person({ ${ID_PROP} ${propCode} name: "C" }); +}`, + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ✅ If/else if with final return, property in all paths + "if else if with final return property in all paths": [ + lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + return new Person({ ${ID_PROP} ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ ${ID_PROP} ${propCode} name: "B" }); +} +return new Person({ ${ID_PROP} ${propCode} name: "C" });`, + setter, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ If/else if with final return, missing property in final return + "if else if with final return missing property in final return": [ + lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + return new Person({ ${ID_PROP} ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ ${ID_PROP} ${propCode} name: "B" }); +} +return new Person({ ${ID_PROP} name: "C" });`, + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + }; + }; +} + +/** + * Creates common edge case tests for mismatch rules + */ +export function createMismatchEdgeCaseTests( + propertyKey: PropertyKey, + config: TestConfig, +): TestSuite { + const { rule, ruleName } = config; + const prop = properties[propertyKey]; + + // Build the expected method call context + const expectedError = actorPropertyMismatch(createMethodCallContext(prop)); + + // Find a wrong getter for testing + const wrongGetters = Object.values(properties) + .filter((p) => p.getter !== prop.getter) + .map((p) => p.getter); + const wrongGetter = wrongGetters[0] || "getWrongUri"; + + const createLocalPropertyCode = (getter: string) => + createPropertyAssignment(prop, { getter }); + const propCode = createLocalPropertyCode(prop.getter); + const wrongPropCode = createLocalPropertyCode(wrongGetter); + return { + // ✅ Ternary with correct getter in both branches + "ternary with correct getter in both branches": [ + lintTest({ + code: createActorDispatcherCode(nestedTernaryCode(propCode, propCode)), + rule, + ruleName, + }), + true, + ], + + // ❌ Ternary with wrong getter in consequent + "ternary with wrong getter in consequent": [ + lintTest({ + code: createActorDispatcherCode( + nestedTernaryCode(wrongPropCode, propCode), + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Ternary with wrong getter in alternate + "ternary with wrong getter in alternate": [ + lintTest({ + code: createActorDispatcherCode( + nestedTernaryCode(propCode, wrongPropCode), + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Ternary with wrong getter in both + "ternary with wrong getter in both branches": [ + lintTest({ + code: createActorDispatcherCode( + nestedTernaryCode(wrongPropCode, wrongPropCode), + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ✅ Nested ternary with correct getter + "nested ternary with correct getter": [ + lintTest({ + code: createActorDispatcherCode( + `\ +return condition1 + ? (condition2 + ? new Person({ ${ID_PROP} ${propCode} name: "A" }) + : new Person({ ${ID_PROP} ${propCode} name: "B" })) + : new Person({ ${ID_PROP} ${propCode} name: "C" });`, + ), + rule, + ruleName, + }), + true, + ], + + // ✅ If/else with correct getter in both branches + "if else with correct getter in both branches": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition) { + return new Person({ ${ID_PROP} ${propCode} name: "A" }); +} else { + return new Person({ ${ID_PROP} ${propCode} name: "B" }); +}`, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ If/else with wrong getter in if block + "if else with wrong getter in if block": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition) { + return new Person({ ${ID_PROP} ${wrongPropCode} name: "A" }); +} else { + return new Person({ ${ID_PROP} ${propCode} name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ If/else with wrong getter in else block + "if else with wrong getter in else block": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition) { + return new Person({ ${ID_PROP} ${propCode} name: "A" }); +} else { + return new Person({ ${ID_PROP} ${wrongPropCode} name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ If/else with wrong getter in both blocks + "if else with wrong getter in both blocks": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition) { + return new Person({ ${ID_PROP} ${wrongPropCode} name: "A" }); +} else { + return new Person({ ${ID_PROP} ${wrongPropCode} name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ✅ Nested if with correct getter + "nested if with correct getter": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition1) { + if (condition2) { + return new Person({ ${ID_PROP} ${propCode} name: "A" }); + } else { + return new Person({ ${ID_PROP} ${propCode} name: "B" }); + } +} else { + return new Person({ ${ID_PROP} ${propCode} name: "C" }); +}`, + ), + rule, + ruleName, + }), + true, + ], + + // ✅ If/else if/else with correct getter in all branches + "if else if else with correct getter in all branches": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition1) { + return new Person({ ${ID_PROP} ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ ${ID_PROP} ${propCode} name: "B" }); +} else { + return new Person({ ${ID_PROP} ${propCode} name: "C" }); +}`, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ If/else if/else with wrong getter in else if + "if else if else with wrong getter in else if": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition1) { + return new Person({ ${ID_PROP} ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ ${ID_PROP} ${wrongPropCode} name: "B" }); +} else { + return new Person({ ${ID_PROP} ${propCode} name: "C" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ✅ If/else if with final return, correct getter in all paths + "if else if with final return correct getter in all paths": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition1) { + return new Person({ ${ID_PROP} ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ ${ID_PROP} ${propCode} name: "B" }); +} +return new Person({ ${ID_PROP} ${propCode} name: "C" });`, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ If/else if with final return, wrong getter in final return + "if else if with final return wrong getter in final return": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition1) { + return new Person({ ${ID_PROP} ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ ${ID_PROP} ${propCode} name: "B" }); +} +return new Person({ ${ID_PROP} ${wrongPropCode} name: "C" });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + }; +} + +const nestedTernaryCode = (prop1: string, prop2: string) => + `return condition + ? new Person({ ${ID_PROP} ${prop1} name: "A" }) + : new Person({ ${ID_PROP} ${prop2} name: "B" });`; + +/** + * Creates edge case tests for id required rule + */ +export function createIdRequiredEdgeCaseTests(config: TestConfig): TestSuite { + const { rule, ruleName } = config; + const expectedError = actorPropertyRequired(properties.id); + + return { + // ✅ Ternary with id in both branches + "ternary with id in both branches": [ + lintTest({ + code: createActorDispatcherCode( + `return condition + ? new Person({ ${ID_PROP} name: "A" }) + : new Person({ ${ID_PROP} name: "B" });`, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ Ternary missing id in consequent + "ternary missing id in consequent": [ + lintTest({ + code: createActorDispatcherCode( + `return condition + ? new Person({ name: "A" }) + : new Person({ ${ID_PROP} name: "B" });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Ternary missing id in alternate + "ternary missing id in alternate": [ + lintTest({ + code: createActorDispatcherCode( + `return condition + ? new Person({ ${ID_PROP} name: "A" }) + : new Person({ name: "B" });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Ternary missing id in both + "ternary missing id in both branches": [ + lintTest({ + code: createActorDispatcherCode( + `return condition + ? new Person({ name: "A" }) + : new Person({ name: "B" });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ✅ Nested ternary with id + "nested ternary with id": [ + lintTest({ + code: createActorDispatcherCode( + `return condition1 + ? (condition2 + ? new Person({ ${ID_PROP} name: "A" }) + : new Person({ ${ID_PROP} name: "B" })) + : new Person({ ${ID_PROP} name: "C" });`, + ), + rule, + ruleName, + }), + true, + ], + + // ✅ If/else with id in both branches + "if else with id in both branches": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition) { + return new Person({ ${ID_PROP} name: "A" }); +} else { + return new Person({ ${ID_PROP} name: "B" }); +}`, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ If/else missing id in if block + "if else missing id in if block": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition) { + return new Person({ name: "A" }); +} else { + return new Person({ ${ID_PROP} name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ If/else missing id in else block + "if else missing id in else block": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition) { + return new Person({ ${ID_PROP} name: "A" }); +} else { + return new Person({ name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ If/else missing id in both blocks + "if else missing id in both blocks": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition) { + return new Person({ name: "A" }); +} else { + return new Person({ name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ✅ Nested if with id + "nested if with id": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition1) { + if (condition2) { + return new Person({ ${ID_PROP} name: "A" }); + } else { + return new Person({ ${ID_PROP} name: "B" }); + } +} else { + return new Person({ ${ID_PROP} name: "C" }); +}`, + ), + rule, + ruleName, + }), + true, + ], + + // ✅ If/else if/else with id in all branches + "if else if else with id in all branches": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition1) { + return new Person({ ${ID_PROP} name: "A" }); +} else if (condition2) { + return new Person({ ${ID_PROP} name: "B" }); +} else { + return new Person({ ${ID_PROP} name: "C" }); +}`, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ If/else if/else missing id in else if + "if else if else missing id in else if": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition1) { + return new Person({ ${ID_PROP} name: "A" }); +} else if (condition2) { + return new Person({ name: "B" }); +} else { + return new Person({ ${ID_PROP} name: "C" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ✅ If/else if with final return, id in all paths + "if else if with final return id in all paths": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition1) { + return new Person({ ${ID_PROP} name: "A" }); +} else if (condition2) { + return new Person({ ${ID_PROP} name: "B" }); +} +return new Person({ ${ID_PROP} name: "C" });`, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ If/else if with final return, missing id in final return + "if else if with final return missing id in final return": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition1) { + return new Person({ ${ID_PROP} name: "A" }); +} else if (condition2) { + return new Person({ ${ID_PROP} name: "B" }); +} +return new Person({ name: "C" });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + }; +} + +/** + * Creates edge case tests for id mismatch rule + */ +export function createIdMismatchEdgeCaseTests(config: TestConfig): TestSuite { + const { rule, ruleName } = config; + const expectedError = actorPropertyMismatch( + createMethodCallContext(properties.id), + ); + + return { + // ✅ Ternary with correct getter in both branches + "ternary with correct getter in both branches": [ + lintTest({ + code: createActorDispatcherCode( + `return condition + ? new Person({ ${ID_PROP} name: "A" }) + : new Person({ ${ID_PROP} name: "B" });`, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ Ternary with wrong getter in consequent + "ternary with wrong getter in consequent": [ + lintTest({ + code: createActorDispatcherCode( + `return condition + ? new Person({ id: ctx.getFollowingUri(identifier), name: "A" }) + : new Person({ ${ID_PROP} name: "B" });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Ternary with wrong getter in alternate + "ternary with wrong getter in alternate": [ + lintTest({ + code: createActorDispatcherCode( + `return condition + ? new Person({ ${ID_PROP} name: "A" }) + : new Person({ id: ctx.getFollowingUri(identifier), name: "B" });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Ternary with wrong getter in both + "ternary with wrong getter in both branches": [ + lintTest({ + code: createActorDispatcherCode( + `return condition + ? new Person({ id: ctx.getFollowingUri(identifier), name: "A" }) + : new Person({ id: ctx.getFollowingUri(identifier), name: "B" });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ✅ Nested ternary with correct getter + "nested ternary with correct getter": [ + lintTest({ + code: createActorDispatcherCode( + `return condition1 + ? (condition2 + ? new Person({ ${ID_PROP} name: "A" }) + : new Person({ ${ID_PROP} name: "B" })) + : new Person({ ${ID_PROP} name: "C" });`, + ), + rule, + ruleName, + }), + true, + ], + + // ✅ If/else with correct getter in both branches + "if else with correct getter in both branches": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition) { + return new Person({ ${ID_PROP} name: "A" }); +} else { + return new Person({ ${ID_PROP} name: "B" }); +}`, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ If/else with wrong getter in if block + "if else with wrong getter in if block": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getFollowingUri(identifier), name: "A" }); +} else { + return new Person({ ${ID_PROP} name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ If/else with wrong getter in else block + "if else with wrong getter in else block": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition) { + return new Person({ ${ID_PROP} name: "A" }); +} else { + return new Person({ id: ctx.getFollowingUri(identifier), name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ If/else with wrong getter in both blocks + "if else with wrong getter in both blocks": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getFollowingUri(identifier), name: "A" }); +} else { + return new Person({ id: ctx.getFollowingUri(identifier), name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ✅ Nested if with correct getter + "nested if with correct getter": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition1) { + if (condition2) { + return new Person({ ${ID_PROP} name: "A" }); + } else { + return new Person({ ${ID_PROP} name: "B" }); + } +} else { + return new Person({ ${ID_PROP} name: "C" }); +}`, + ), + rule, + ruleName, + }), + true, + ], + + // ✅ If/else if/else with correct getter in all branches + "if else if else with correct getter in all branches": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition1) { + return new Person({ ${ID_PROP} name: "A" }); +} else if (condition2) { + return new Person({ ${ID_PROP} name: "B" }); +} else { + return new Person({ ${ID_PROP} name: "C" }); +}`, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ If/else if/else with wrong getter in else if + "if else if else with wrong getter in else if": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition1) { + return new Person({ ${ID_PROP} name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getFollowingUri(identifier), name: "B" }); +} else { + return new Person({ ${ID_PROP} name: "C" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ✅ If/else if with final return, correct getter in all paths + "if else if with final return correct getter in all paths": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition1) { + return new Person({ ${ID_PROP} name: "A" }); +} else if (condition2) { + return new Person({ ${ID_PROP} name: "B" }); +} +return new Person({ ${ID_PROP} name: "C" });`, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ If/else if with final return, wrong getter in final return + "if else if with final return wrong getter in final return": [ + lintTest({ + code: createActorDispatcherCode( + `\ +if (condition1) { + return new Person({ ${ID_PROP} name: "A" }); +} else if (condition2) { + return new Person({ ${ID_PROP} name: "B" }); +} +return new Person({ id: ctx.getFollowingUri(identifier), name: "C" });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + }; +} diff --git a/packages/lint/src/lib/test.ts b/packages/lint/src/lib/test.ts new file mode 100644 index 000000000..5bec4c805 --- /dev/null +++ b/packages/lint/src/lib/test.ts @@ -0,0 +1,157 @@ +import { isNil } from "@fxts/core"; +import * as tsParser from "@typescript-eslint/parser"; +import { Linter, type Rule } from "eslint"; +import assert from "node:assert/strict"; +import { FEDERATION_SETUP } from "./const.ts"; + +const LINT_PLUGIN_NAME = "fedify-lint-test"; + +function testDenoLint( + { + code, + rule, + ruleName, + federationSetup = FEDERATION_SETUP, + expectedError, + }: { + code: string; + rule: Deno.lint.Rule; + ruleName: string; + federationSetup?: string; + expectedError?: string; + }, +) { + const plugin: Deno.lint.Plugin = { + name: LINT_PLUGIN_NAME, + rules: { [ruleName]: rule }, + }; + + const diagnostics = Deno.lint.runPlugin( + plugin, + ruleName + ".test.ts", + `${federationSetup ?? federationSetup}\n\n${code}`, + ); + + if (isNil(expectedError)) { + assert.equal( + diagnostics.length, + 0, + "Should not report issues when id property is present but found:", + ); + } else { + assert.ok( + diagnostics.length > 0, + "Expected at least one diagnostic error but found none", + ); + const lintId = `${LINT_PLUGIN_NAME}/${ruleName}`; + const matched = diagnostics.some((diag) => + diag.message.includes(expectedError) + ); + assert.ok( + matched, + `Expected ${lintId} to report but it did not.`, + ); + } +} + +function testEslint( + { + code, + rule, + ruleName, + federationSetup = FEDERATION_SETUP, + expectedError, + }: { + code: string; + rule: Rule.RuleModule; + ruleName: string; + federationSetup?: string; + expectedError?: string; + }, +) { + const linter = new Linter({ configType: "flat" }); + + const config = [{ + files: ["**/*.ts"], + plugins: { + "fedify-test": { + rules: { [ruleName]: rule }, + }, + }, + rules: { + [`fedify-test/${ruleName}`]: expectedError ? "error" : "off", + }, + languageOptions: { + parser: tsParser, + ecmaVersion: "latest", + sourceType: "module", + }, + } as Linter.Config]; + + const fullCode = `${federationSetup ?? federationSetup}\n\n${code}`; + const results = linter.verify(fullCode, config, `${ruleName}.test.ts`); + + if (isNil(expectedError)) { + assert.equal( + results.length, + 0, + `Should not report issues but found: ${ + results.map((r) => r.message).join(", ") + }`, + ); + } else { + assert.ok( + results.length > 0, + "Expected at least one diagnostic error but found none", + ); + const matched = results.some((r) => + r.message.includes(expectedError) || + r.ruleId === `fedify-test/${ruleName}` + ); + assert.ok( + matched, + `Expected fedify-test/${ruleName} to report but it did not. Got: ${ + results.map((r) => r.message).join(", ") + }`, + ); + } +} + +export default function lintTest( + { + code, + rule: { deno, eslint }, + ruleName, + federationSetup = FEDERATION_SETUP, + expectedError, + }: { + code: string; + rule: { + deno: Deno.lint.Rule; + eslint: Rule.RuleModule; + }; + ruleName: string; + federationSetup?: string; + expectedError?: string; + }, +) { + if ("Deno" in globalThis) { + return () => + testDenoLint({ + code, + rule: deno, + ruleName, + federationSetup, + expectedError, + }); + } else { + return () => + testEslint({ + code, + rule: eslint, + ruleName, + federationSetup, + expectedError, + }); + } +} diff --git a/packages/lint/src/lib/tracker.ts b/packages/lint/src/lib/tracker.ts new file mode 100644 index 000000000..4a7b8293e --- /dev/null +++ b/packages/lint/src/lib/tracker.ts @@ -0,0 +1,60 @@ +import type { + CallExpression, + Expression, + VariableDeclarator, +} from "./types.ts"; + +/** + * Helper to track variable names that store the result of createFederation() or createFederationBuilder() calls + */ +export function trackFederationVariables() { + const federationVariables = new Set(); + + const isFederationObject = ( + obj: Expression, + ): boolean => { + switch (obj.type) { + case "Identifier": + return federationVariables.has(obj.name); + case "CallExpression": + // Check if it's a direct createFederation call + if (isCreateFederation(obj)) return true; + // Check if it's a chained method call on a federation object + if (obj.callee.type === "MemberExpression") { + return isFederationObject(obj.callee.object); + } + return false; + case "MemberExpression": + return isFederationObject(obj.object); + } + return false; + }; + + return { + VariableDeclarator(node: VariableDeclarator): void { + const init = node.init; + const id = node.id; + + if (init?.type === "CallExpression") { + if ( + isCreateFederation(init) && + id.type === "Identifier" + ) { + federationVariables.add(id.name); + } + } + }, + + isFederationVariable(name: string): boolean { + return federationVariables.has(name); + }, + + isFederationObject, + }; +} + +const isCreateFederation = ( + node: CallExpression, +): boolean => + node.callee.type === "Identifier" && + /^create(Federation|FederationBuilder)$/i.test(node.callee.name); diff --git a/packages/lint/src/lib/types.ts b/packages/lint/src/lib/types.ts new file mode 100644 index 000000000..1806f9241 --- /dev/null +++ b/packages/lint/src/lib/types.ts @@ -0,0 +1,117 @@ +import type * as ESTree from "estree"; + +export type BlockStatement = + | Deno.lint.BlockStatement + | ESTree.BlockStatement; +export type CallExpression = Deno.lint.CallExpression | ESTree.CallExpression; +export type ConditionalExpression = + | Deno.lint.ConditionalExpression + | ESTree.ConditionalExpression; +export type Expression = + | Deno.lint.Expression + | ESTree.Expression + | ESTree.Super; +export type Identifier = Deno.lint.Identifier | ESTree.Identifier; +export type MemberExpression = + | Deno.lint.MemberExpression + | ESTree.MemberExpression; +export type NewExpression = Deno.lint.NewExpression | ESTree.NewExpression; +export type Node = Deno.lint.Node | ESTree.Node; +export type ObjectExpression = + | Deno.lint.ObjectExpression + | ESTree.ObjectExpression; +export type ObjectPattern = + | Deno.lint.ObjectPattern + | ESTree.ObjectPattern; +export type Parameter = Deno.lint.Parameter | ESTree.Pattern; +export type PrivateIdentifier = + | Deno.lint.PrivateIdentifier + | ESTree.PrivateIdentifier; +export type Property = Deno.lint.Property | ESTree.Property; +export type ReturnStatement = + | Deno.lint.ReturnStatement + | ESTree.ReturnStatement; +export type SpreadElement = Deno.lint.SpreadElement | ESTree.SpreadElement; +export type Statement = Deno.lint.Statement | ESTree.Statement; +export type VariableDeclarator = + | Deno.lint.VariableDeclarator + | ESTree.VariableDeclarator; + +export type AssignmentPattern = + | Deno.lint.AssignmentPattern + | ESTree.AssignmentPattern; + +export type FunctionNode = + | Deno.lint.ArrowFunctionExpression + | Deno.lint.FunctionExpression + | ESTree.ArrowFunctionExpression + | ESTree.FunctionExpression; + +export type CallMemberExpression = CallExpression & { + callee: MemberExpression; +}; + +export type CallMemberExpressionWithIdentified = CallExpression & { + callee: MemberExpression & { + property: Identifier; + }; +}; + +export type PropertyChecker = ( + prop: + | Property + | SpreadElement, +) => boolean; + +/** + * Configuration for nested property wrappers. + * Used when a property needs to be wrapped in a class instance + * (e.g., `new Endpoints({...})`). + */ +interface NestedPropertyConfig { + /** Parent property name (e.g., "endpoints") */ + parent: string; + /** Wrapper class name (e.g., "Endpoints") */ + wrapper: string; +} + +/** + * Configuration for an actor property. + */ +export interface PropertyConfig { + /** Property name (e.g., "id", "sharedInbox") */ + name: string; + /** + * Full property path for lint rules + * (e.g., ["id"], ["endpoints", "sharedInbox"]) + */ + path: readonly string[]; + /** Context method name to get the URI (e.g., "getActorUri", "getInboxUri") */ + getter: string; + /** + * Dispatcher/Listener method name + * (e.g., "setActorDispatcher", "setInboxListeners") + */ + setter: string; + /** Whether the getter requires an identifier parameter (default: true) */ + requiresIdentifier: boolean; + /** Nested property configuration, if this property is nested inside another */ + nested?: NestedPropertyConfig; + /** Whether this is a key-related property (uses getActorKeyPairs) */ + isKeyProperty?: boolean; +} + +/** + * Context for method call validation. + */ +export interface MethodCallContext { + path: string; + ctxName: string; + idName: string; + methodName: string; + requiresIdentifier: boolean; +} + +export interface WithIdentifierKey { + key: Identifier & { name: T }; +} diff --git a/packages/lint/src/lib/utils.ts b/packages/lint/src/lib/utils.ts new file mode 100644 index 000000000..cda9ea40a --- /dev/null +++ b/packages/lint/src/lib/utils.ts @@ -0,0 +1,73 @@ +/** + * Returns the value of the key in the object. + */ +export function getProp( + key: T, +): (obj: S) => S[T]; +export function getProp( + key: T, +): (obj: S) => S[T] { + return (obj: S) => obj[key]; +} + +/** + * Returns the value of the key in the object if it exists, + * otherwise returns second argument as default value. + */ +export function getPropOr( + key: T, + defaultValue?: V, +): (obj: S) => T extends keyof S ? S[T] : V { + return (obj: S) => + ((obj as Record)[key] ?? defaultValue) as // + (T extends keyof S ? S[T] : V); +} + +export const eq = (value: S) => (other: T): boolean => + value === other; + +export const getArticle = (word: string): string => + /^[aeiou]/i.test(word) ? "an" : "a"; + +export const endsWith = (suffix: string) => (str: string): boolean => + str.endsWith(suffix); + +export const replace: { + (searchValue: string | RegExp, replaceValue: string): (str: string) => string; + ( + searchValue: string | RegExp, + replacer: (substring: string, ...args: unknown[]) => string, + ): (str: string) => string; +} = ( + searchValue: string | RegExp, + replaceValue: string | ((substring: string, ...args: unknown[]) => string), +): (str: string) => string => +(str: string): string => + str.replace( + searchValue, + // @ts-ignore tsc cannot infer the type here + replaceValue, + ); + +export function cases( + pred: (value: T) => value is T1 & T, + ifTrue: (value: T1) => R1, + ifFalse: (value: T2) => R2, +): (value: T) => R1 | R2; +export function cases( + pred: (value: T) => value is T1 & T, + ifTrue: (value: T1) => R, + ifFalse: (value: T2) => R, +): (value: unknown) => R | R; +export function cases( + pred: (value: T) => boolean, + ifTrue: (value: T) => R, + ifFalse: (value: T) => R, +): (value: T) => R; +export function cases( + pred: (value: T) => boolean, + ifTrue: (value: T) => R, + ifFalse: (value: T) => R, +): (value: T) => R { + return (value: T): R => pred(value) ? ifTrue(value) : ifFalse(value); +} diff --git a/packages/lint/src/mod.ts b/packages/lint/src/mod.ts new file mode 100644 index 000000000..b745c2aa8 --- /dev/null +++ b/packages/lint/src/mod.ts @@ -0,0 +1,93 @@ +import { RULE_IDS } from "./lib/const.ts"; +import { + deno as actorAssertionMethodRequired, +} from "./rules/actor-assertion-method-required.ts"; +import { + deno as actorFeaturedPropertyMismatch, +} from "./rules/actor-featured-property-mismatch.ts"; +import { + deno as actorFeaturedPropertyRequired, +} from "./rules/actor-featured-property-required.ts"; +import { + deno as actorFeaturedTagsPropertyMismatch, +} from "./rules/actor-featured-tags-property-mismatch.ts"; +import { + deno as actorFeaturedTagsPropertyRequired, +} from "./rules/actor-featured-tags-property-required.ts"; +import { + deno as actorFollowersPropertyMismatch, +} from "./rules/actor-followers-property-mismatch.ts"; +import { + deno as actorFollowersPropertyRequired, +} from "./rules/actor-followers-property-required.ts"; +import { + deno as actorFollowingPropertyMismatch, +} from "./rules/actor-following-property-mismatch.ts"; +import { + deno as actorFollowingPropertyRequired, +} from "./rules/actor-following-property-required.ts"; +import { deno as actorIdMismatch } from "./rules/actor-id-mismatch.ts"; +import { deno as actorIdRequired } from "./rules/actor-id-required.ts"; +import { + deno as actorInboxPropertyMismatch, +} from "./rules/actor-inbox-property-mismatch.ts"; +import { + deno as actorInboxPropertyRequired, +} from "./rules/actor-inbox-property-required.ts"; +import { + deno as actorLikedPropertyMismatch, +} from "./rules/actor-liked-property-mismatch.ts"; +import { + deno as actorLikedPropertyRequired, +} from "./rules/actor-liked-property-required.ts"; +import { + deno as actorOutboxPropertyMismatch, +} from "./rules/actor-outbox-property-mismatch.ts"; +import { + deno as actorOutboxPropertyRequired, +} from "./rules/actor-outbox-property-required.ts"; +import { + deno as actorPublicKeyRequired, +} from "./rules/actor-public-key-required.ts"; +import { + deno as actorSharedInboxPropertyMismatch, +} from "./rules/actor-shared-inbox-property-mismatch.ts"; +import { + deno as actorSharedInboxPropertyRequired, +} from "./rules/actor-shared-inbox-property-required.ts"; +import { + deno as collectionFiltering, +} from "./rules/collection-filtering-not-implemented.ts"; + +const plugin: Deno.lint.Plugin = { + name: "fedify-lint", + rules: { + [RULE_IDS.actorIdMismatch]: actorIdMismatch, + [RULE_IDS.actorIdRequired]: actorIdRequired, + [RULE_IDS.actorFollowingPropertyRequired]: actorFollowingPropertyRequired, + [RULE_IDS.actorFollowingPropertyMismatch]: actorFollowingPropertyMismatch, + [RULE_IDS.actorFollowersPropertyRequired]: actorFollowersPropertyRequired, + [RULE_IDS.actorFollowersPropertyMismatch]: actorFollowersPropertyMismatch, + [RULE_IDS.actorOutboxPropertyRequired]: actorOutboxPropertyRequired, + [RULE_IDS.actorOutboxPropertyMismatch]: actorOutboxPropertyMismatch, + [RULE_IDS.actorLikedPropertyRequired]: actorLikedPropertyRequired, + [RULE_IDS.actorLikedPropertyMismatch]: actorLikedPropertyMismatch, + [RULE_IDS.actorFeaturedPropertyRequired]: actorFeaturedPropertyRequired, + [RULE_IDS.actorFeaturedPropertyMismatch]: actorFeaturedPropertyMismatch, + [RULE_IDS.actorFeaturedTagsPropertyRequired]: + actorFeaturedTagsPropertyRequired, + [RULE_IDS.actorFeaturedTagsPropertyMismatch]: + actorFeaturedTagsPropertyMismatch, + [RULE_IDS.actorInboxPropertyRequired]: actorInboxPropertyRequired, + [RULE_IDS.actorInboxPropertyMismatch]: actorInboxPropertyMismatch, + [RULE_IDS.actorSharedInboxPropertyRequired]: + actorSharedInboxPropertyRequired, + [RULE_IDS.actorSharedInboxPropertyMismatch]: + actorSharedInboxPropertyMismatch, + [RULE_IDS.actorPublicKeyRequired]: actorPublicKeyRequired, + [RULE_IDS.actorAssertionMethodRequired]: actorAssertionMethodRequired, + [RULE_IDS.collectionFilteringNotImplemented]: collectionFiltering, + }, +}; + +export default plugin; diff --git a/packages/lint/src/rules/actor-assertion-method-required.ts b/packages/lint/src/rules/actor-assertion-method-required.ts new file mode 100644 index 000000000..86404938f --- /dev/null +++ b/packages/lint/src/rules/actor-assertion-method-required.ts @@ -0,0 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required.ts"; + +export const deno = createRequiredRuleDeno( + properties.assertionMethod, +); +export const eslint = createRequiredRuleEslint( + properties.assertionMethod, +); diff --git a/packages/lint/src/rules/actor-featured-property-mismatch.ts b/packages/lint/src/rules/actor-featured-property-mismatch.ts new file mode 100644 index 000000000..f47cf8047 --- /dev/null +++ b/packages/lint/src/rules/actor-featured-property-mismatch.ts @@ -0,0 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch.ts"; + +export const deno = createMismatchRuleDeno( + properties.featured, +); +export const eslint = createMismatchRuleEslint( + properties.featured, +); diff --git a/packages/lint/src/rules/actor-featured-property-required.ts b/packages/lint/src/rules/actor-featured-property-required.ts new file mode 100644 index 000000000..1637c158b --- /dev/null +++ b/packages/lint/src/rules/actor-featured-property-required.ts @@ -0,0 +1,8 @@ +import { properties } from "../lib/const.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required.ts"; + +export const deno = createRequiredRuleDeno(properties.featured); +export const eslint = createRequiredRuleEslint(properties.featured); diff --git a/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts b/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts new file mode 100644 index 000000000..ed0e33eb2 --- /dev/null +++ b/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts @@ -0,0 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch.ts"; + +export const deno = createMismatchRuleDeno( + properties.featuredTags, +); +export const eslint = createMismatchRuleEslint( + properties.featuredTags, +); diff --git a/packages/lint/src/rules/actor-featured-tags-property-required.ts b/packages/lint/src/rules/actor-featured-tags-property-required.ts new file mode 100644 index 000000000..3efddecda --- /dev/null +++ b/packages/lint/src/rules/actor-featured-tags-property-required.ts @@ -0,0 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required.ts"; + +export const deno = createRequiredRuleDeno( + properties.featuredTags, +); +export const eslint = createRequiredRuleEslint( + properties.featuredTags, +); diff --git a/packages/lint/src/rules/actor-followers-property-mismatch.ts b/packages/lint/src/rules/actor-followers-property-mismatch.ts new file mode 100644 index 000000000..6fd2ed7c7 --- /dev/null +++ b/packages/lint/src/rules/actor-followers-property-mismatch.ts @@ -0,0 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch.ts"; + +export const deno = createMismatchRuleDeno( + properties.followers, +); +export const eslint = createMismatchRuleEslint( + properties.followers, +); diff --git a/packages/lint/src/rules/actor-followers-property-required.ts b/packages/lint/src/rules/actor-followers-property-required.ts new file mode 100644 index 000000000..4b06e148c --- /dev/null +++ b/packages/lint/src/rules/actor-followers-property-required.ts @@ -0,0 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required.ts"; + +export const deno = createRequiredRuleDeno( + properties.followers, +); +export const eslint = createRequiredRuleEslint( + properties.followers, +); diff --git a/packages/lint/src/rules/actor-following-property-mismatch.ts b/packages/lint/src/rules/actor-following-property-mismatch.ts new file mode 100644 index 000000000..1accdee8e --- /dev/null +++ b/packages/lint/src/rules/actor-following-property-mismatch.ts @@ -0,0 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch.ts"; + +export const deno = createMismatchRuleDeno( + properties.following, +); +export const eslint = createMismatchRuleEslint( + properties.following, +); diff --git a/packages/lint/src/rules/actor-following-property-required.ts b/packages/lint/src/rules/actor-following-property-required.ts new file mode 100644 index 000000000..38619e7b1 --- /dev/null +++ b/packages/lint/src/rules/actor-following-property-required.ts @@ -0,0 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required.ts"; + +export const deno = createRequiredRuleDeno( + properties.following, +); +export const eslint = createRequiredRuleEslint( + properties.following, +); diff --git a/packages/lint/src/rules/actor-id-mismatch.ts b/packages/lint/src/rules/actor-id-mismatch.ts new file mode 100644 index 000000000..f4203ca0d --- /dev/null +++ b/packages/lint/src/rules/actor-id-mismatch.ts @@ -0,0 +1,8 @@ +import { properties } from "../lib/const.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch.ts"; + +export const deno = createMismatchRuleDeno(properties.id); +export const eslint = createMismatchRuleEslint(properties.id); diff --git a/packages/lint/src/rules/actor-id-required.ts b/packages/lint/src/rules/actor-id-required.ts new file mode 100644 index 000000000..7d4318e52 --- /dev/null +++ b/packages/lint/src/rules/actor-id-required.ts @@ -0,0 +1,8 @@ +import { properties } from "../lib/const.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required.ts"; + +export const deno = createRequiredRuleDeno(properties.id); +export const eslint = createRequiredRuleEslint(properties.id); diff --git a/packages/lint/src/rules/actor-inbox-property-mismatch.ts b/packages/lint/src/rules/actor-inbox-property-mismatch.ts new file mode 100644 index 000000000..a1f2c159d --- /dev/null +++ b/packages/lint/src/rules/actor-inbox-property-mismatch.ts @@ -0,0 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch.ts"; + +export const deno = createMismatchRuleDeno( + properties.inbox, +); +export const eslint = createMismatchRuleEslint( + properties.inbox, +); diff --git a/packages/lint/src/rules/actor-inbox-property-required.ts b/packages/lint/src/rules/actor-inbox-property-required.ts new file mode 100644 index 000000000..33b794dea --- /dev/null +++ b/packages/lint/src/rules/actor-inbox-property-required.ts @@ -0,0 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required.ts"; + +export const deno = createRequiredRuleDeno( + properties.inbox, +); +export const eslint = createRequiredRuleEslint( + properties.inbox, +); diff --git a/packages/lint/src/rules/actor-liked-property-mismatch.ts b/packages/lint/src/rules/actor-liked-property-mismatch.ts new file mode 100644 index 000000000..648c1f868 --- /dev/null +++ b/packages/lint/src/rules/actor-liked-property-mismatch.ts @@ -0,0 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch.ts"; + +export const deno = createMismatchRuleDeno( + properties.liked, +); +export const eslint = createMismatchRuleEslint( + properties.liked, +); diff --git a/packages/lint/src/rules/actor-liked-property-required.ts b/packages/lint/src/rules/actor-liked-property-required.ts new file mode 100644 index 000000000..31cd7d541 --- /dev/null +++ b/packages/lint/src/rules/actor-liked-property-required.ts @@ -0,0 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required.ts"; + +export const deno = createRequiredRuleDeno( + properties.liked, +); +export const eslint = createRequiredRuleEslint( + properties.liked, +); diff --git a/packages/lint/src/rules/actor-outbox-property-mismatch.ts b/packages/lint/src/rules/actor-outbox-property-mismatch.ts new file mode 100644 index 000000000..aa4698fb1 --- /dev/null +++ b/packages/lint/src/rules/actor-outbox-property-mismatch.ts @@ -0,0 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch.ts"; + +export const deno = createMismatchRuleDeno( + properties.outbox, +); +export const eslint = createMismatchRuleEslint( + properties.outbox, +); diff --git a/packages/lint/src/rules/actor-outbox-property-required.ts b/packages/lint/src/rules/actor-outbox-property-required.ts new file mode 100644 index 000000000..0f8866ea8 --- /dev/null +++ b/packages/lint/src/rules/actor-outbox-property-required.ts @@ -0,0 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required.ts"; + +export const deno = createRequiredRuleDeno( + properties.outbox, +); +export const eslint = createRequiredRuleEslint( + properties.outbox, +); diff --git a/packages/lint/src/rules/actor-public-key-required.ts b/packages/lint/src/rules/actor-public-key-required.ts new file mode 100644 index 000000000..89f5fe35d --- /dev/null +++ b/packages/lint/src/rules/actor-public-key-required.ts @@ -0,0 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required.ts"; + +export const deno = createRequiredRuleDeno( + properties.publicKey, +); +export const eslint = createRequiredRuleEslint( + properties.publicKey, +); diff --git a/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts b/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts new file mode 100644 index 000000000..544d1bddc --- /dev/null +++ b/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts @@ -0,0 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch.ts"; + +export const deno = createMismatchRuleDeno( + properties.sharedInbox, +); +export const eslint = createMismatchRuleEslint( + properties.sharedInbox, +); diff --git a/packages/lint/src/rules/actor-shared-inbox-property-required.ts b/packages/lint/src/rules/actor-shared-inbox-property-required.ts new file mode 100644 index 000000000..7b09a74cc --- /dev/null +++ b/packages/lint/src/rules/actor-shared-inbox-property-required.ts @@ -0,0 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required.ts"; + +export const deno = createRequiredRuleDeno( + properties.sharedInbox, +); +export const eslint = createRequiredRuleEslint( + properties.sharedInbox, +); diff --git a/packages/lint/src/rules/collection-filtering-not-implemented.ts b/packages/lint/src/rules/collection-filtering-not-implemented.ts new file mode 100644 index 000000000..b47281e4c --- /dev/null +++ b/packages/lint/src/rules/collection-filtering-not-implemented.ts @@ -0,0 +1,137 @@ +import type { Rule } from "eslint"; +import { + COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR, + COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR as message, +} from "../lib/messages.ts"; +import { hasMinParams, isFunction } from "../lib/pred.ts"; +import { trackFederationVariables } from "../lib/tracker.ts"; +import type { + CallExpression, + CallMemberExpressionWithIdentified, +} from "../lib/types.ts"; + +export const COLLECTION_FILTERING_NOT_IMPLEMENTED = + "collection-filtering-not-implemented"; + +/** + * The followers dispatcher method that supports filtering. + * Only setFollowersDispatcher uses the filter parameter for + * followers collection synchronization. + * See: https://fedify.dev/manual/collections#filtering-by-server + */ +const FILTER_NEEDED = ["setFollowersDispatcher"]; + +/** + * Checks if a node is a setFollowersDispatcher call. + */ +const isFollowersDispatcherCall = ( + node: CallExpression, +): node is CallMemberExpressionWithIdentified => + "callee" in node && + node.callee && + node.callee.type === "MemberExpression" && + node.callee.property.type === "Identifier" && + FILTER_NEEDED.includes(node.callee.property.name); + +/** + * Checks if a function node has the filter parameter (4th parameter). + * CollectionDispatcher signature: (context, identifier, cursor, filter?) => ... + */ +const hasFilterParameter = hasMinParams(4); + +/** + * Lint rule: collection-filtering-not-implemented + * + * Warns when setFollowersDispatcher doesn't implement filtering. + * The followers dispatcher should accept a 4th parameter (filter/baseUri) to + * support server-side filtering for followers collection synchronization. + * See: https://fedify.dev/manual/collections#filtering-by-server + * + * @example Good: + * ```typescript ignore + * federation.setFollowersDispatcher( + * "/users/{identifier}/followers", + * async (ctx, identifier, cursor, filter) => { + * // filter is a URL representing the base URI to filter by + * if (filter != null) { + * // Filter followers by server + * } + * return { items: [] }; + * } + * ); + * ``` + * + * @example Bad: + * ```typescript ignore + * federation.setFollowersDispatcher( + * "/users/{identifier}/followers", + * async (ctx, identifier, cursor) => { + * // No filter parameter - will cause warning + * return { items: [] }; + * } + * ); + * ``` + */ +export const deno: Deno.lint.Rule = { + create(context) { + const federationTracker = trackFederationVariables(); + + return { + VariableDeclarator: federationTracker.VariableDeclarator, + + CallExpression(node) { + // Check if it's a setFollowersDispatcher call on a federation object + if (!isFollowersDispatcherCall(node)) return; + if (!federationTracker.isFederationObject(node.callee.object)) return; + + // Get the dispatcher callback (2nd argument) + const dispatcherArg = node.arguments[1]; + if (!isFunction(dispatcherArg)) return; + + // Check if the callback has the filter parameter + if (!hasFilterParameter(dispatcherArg)) { + context.report({ + node: dispatcherArg, + message, + }); + } + }, + }; + }, +}; + +export const eslint: Rule.RuleModule = { + meta: { + type: "suggestion", + docs: { + description: "Ensure followers dispatcher implements filtering", + }, + schema: [], + messages: { + filterRequired: COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR, + }, + }, + create(context) { + const federationTracker = trackFederationVariables(); + + return { + VariableDeclarator: federationTracker.VariableDeclarator, + CallExpression(node): void { + if (!isFollowersDispatcherCall(node)) return; + if (!federationTracker.isFederationObject(node.callee.object)) return; + + // Get the dispatcher callback (2nd argument) + const dispatcherArg = node.arguments[1]; + if (!isFunction(dispatcherArg)) return; + + // Check if the callback has the filter parameter (4th parameter) + if (!hasFilterParameter(dispatcherArg)) { + context.report({ + node: dispatcherArg, + messageId: "filterRequired", + }); + } + }, + }; + }, +}; diff --git a/packages/lint/src/tests/actor-assertion-method-required.test.ts b/packages/lint/src/tests/actor-assertion-method-required.test.ts new file mode 100644 index 000000000..6bc163b88 --- /dev/null +++ b/packages/lint/src/tests/actor-assertion-method-required.test.ts @@ -0,0 +1,16 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createKeyRequiredDispatcherRuleTests, + createKeyRequiredEdgeCaseTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-assertion-method-required.ts"; + +const ruleName = RULE_IDS.actorAssertionMethodRequired; +const config = { rule, ruleName }; + +runTests( + ruleName, + createKeyRequiredDispatcherRuleTests("assertionMethod", config), +); +runTests(ruleName, createKeyRequiredEdgeCaseTests("assertionMethod", config)); diff --git a/packages/lint/src/tests/actor-featured-property-mismatch.test.ts b/packages/lint/src/tests/actor-featured-property-mismatch.test.ts new file mode 100644 index 000000000..bca8148bb --- /dev/null +++ b/packages/lint/src/tests/actor-featured-property-mismatch.test.ts @@ -0,0 +1,13 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createMismatchEdgeCaseTests, + createMismatchRuleTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-featured-property-mismatch.ts"; + +const ruleName = RULE_IDS.actorFeaturedPropertyMismatch; +const config = { rule, ruleName }; + +runTests(ruleName, createMismatchRuleTests("featured", config)); +runTests(ruleName, createMismatchEdgeCaseTests("featured", config)); diff --git a/packages/lint/src/tests/actor-featured-property-required.test.ts b/packages/lint/src/tests/actor-featured-property-required.test.ts new file mode 100644 index 000000000..9e72bd401 --- /dev/null +++ b/packages/lint/src/tests/actor-featured-property-required.test.ts @@ -0,0 +1,16 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createRequiredDispatcherRuleTests, + createRequiredEdgeCaseTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-featured-property-required.ts"; + +const ruleName = RULE_IDS.actorFeaturedPropertyRequired; +const config = { rule, ruleName }; + +runTests(ruleName, createRequiredDispatcherRuleTests("featured", config)); +runTests( + ruleName, + createRequiredEdgeCaseTests("featured", config), +); diff --git a/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts b/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts new file mode 100644 index 000000000..431bfcbe0 --- /dev/null +++ b/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts @@ -0,0 +1,13 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createMismatchEdgeCaseTests, + createMismatchRuleTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-featured-tags-property-mismatch.ts"; + +const ruleName = RULE_IDS.actorFeaturedTagsPropertyMismatch; +const config = { rule, ruleName }; + +runTests(ruleName, createMismatchRuleTests("featuredTags", config)); +runTests(ruleName, createMismatchEdgeCaseTests("featuredTags", config)); diff --git a/packages/lint/src/tests/actor-featured-tags-property-required.test.ts b/packages/lint/src/tests/actor-featured-tags-property-required.test.ts new file mode 100644 index 000000000..f612689ed --- /dev/null +++ b/packages/lint/src/tests/actor-featured-tags-property-required.test.ts @@ -0,0 +1,19 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createRequiredDispatcherRuleTests, + createRequiredEdgeCaseTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-featured-tags-property-required.ts"; + +const ruleName = RULE_IDS.actorFeaturedTagsPropertyRequired; +const config = { rule, ruleName }; + +runTests(ruleName, createRequiredDispatcherRuleTests("featuredTags", config)); +runTests( + ruleName, + createRequiredEdgeCaseTests( + "featuredTags", + config, + ), +); diff --git a/packages/lint/src/tests/actor-followers-property-mismatch.test.ts b/packages/lint/src/tests/actor-followers-property-mismatch.test.ts new file mode 100644 index 000000000..3f111591b --- /dev/null +++ b/packages/lint/src/tests/actor-followers-property-mismatch.test.ts @@ -0,0 +1,13 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createMismatchEdgeCaseTests, + createMismatchRuleTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-followers-property-mismatch.ts"; + +const ruleName = RULE_IDS.actorFollowersPropertyMismatch; +const config = { rule, ruleName }; + +runTests(ruleName, createMismatchRuleTests("followers", config)); +runTests(ruleName, createMismatchEdgeCaseTests("followers", config)); diff --git a/packages/lint/src/tests/actor-followers-property-required.test.ts b/packages/lint/src/tests/actor-followers-property-required.test.ts new file mode 100644 index 000000000..dac8f1f47 --- /dev/null +++ b/packages/lint/src/tests/actor-followers-property-required.test.ts @@ -0,0 +1,16 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createRequiredDispatcherRuleTests, + createRequiredEdgeCaseTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-followers-property-required.ts"; + +const ruleName = RULE_IDS.actorFollowersPropertyRequired; +const config = { rule, ruleName }; + +runTests(ruleName, createRequiredDispatcherRuleTests("followers", config)); +runTests( + ruleName, + createRequiredEdgeCaseTests("followers", config), +); diff --git a/packages/lint/src/tests/actor-following-property-mismatch.test.ts b/packages/lint/src/tests/actor-following-property-mismatch.test.ts new file mode 100644 index 000000000..29c78bdd0 --- /dev/null +++ b/packages/lint/src/tests/actor-following-property-mismatch.test.ts @@ -0,0 +1,13 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createMismatchEdgeCaseTests, + createMismatchRuleTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-following-property-mismatch.ts"; + +const ruleName = RULE_IDS.actorFollowingPropertyMismatch; +const config = { rule, ruleName }; + +runTests(ruleName, createMismatchRuleTests("following", config)); +runTests(ruleName, createMismatchEdgeCaseTests("following", config)); diff --git a/packages/lint/src/tests/actor-following-property-required.test.ts b/packages/lint/src/tests/actor-following-property-required.test.ts new file mode 100644 index 000000000..a1c9bbf45 --- /dev/null +++ b/packages/lint/src/tests/actor-following-property-required.test.ts @@ -0,0 +1,16 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createRequiredDispatcherRuleTests, + createRequiredEdgeCaseTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-following-property-required.ts"; + +const ruleName = RULE_IDS.actorFollowingPropertyRequired; +const config = { rule, ruleName }; + +runTests(ruleName, createRequiredDispatcherRuleTests("following", config)); +runTests( + ruleName, + createRequiredEdgeCaseTests("following", config), +); diff --git a/packages/lint/src/tests/actor-id-mismatch.test.ts b/packages/lint/src/tests/actor-id-mismatch.test.ts new file mode 100644 index 000000000..b6f06a77b --- /dev/null +++ b/packages/lint/src/tests/actor-id-mismatch.test.ts @@ -0,0 +1,13 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createIdMismatchEdgeCaseTests, + createIdMismatchRuleTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-id-mismatch.ts"; + +const ruleName = RULE_IDS.actorIdMismatch; +const config = { rule, ruleName }; + +runTests(ruleName, createIdMismatchRuleTests(config)); +runTests(ruleName, createIdMismatchEdgeCaseTests(config)); diff --git a/packages/lint/src/tests/actor-id-required.test.ts b/packages/lint/src/tests/actor-id-required.test.ts new file mode 100644 index 000000000..de6c2a8b5 --- /dev/null +++ b/packages/lint/src/tests/actor-id-required.test.ts @@ -0,0 +1,13 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createIdRequiredEdgeCaseTests, + createIdRequiredRuleTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-id-required.ts"; + +const ruleName = RULE_IDS.actorIdRequired; +const config = { rule, ruleName }; + +runTests(ruleName, createIdRequiredRuleTests(config)); +runTests(ruleName, createIdRequiredEdgeCaseTests(config)); diff --git a/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts new file mode 100644 index 000000000..b3494d192 --- /dev/null +++ b/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts @@ -0,0 +1,13 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createMismatchEdgeCaseTests, + createMismatchRuleTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-inbox-property-mismatch.ts"; + +const ruleName = RULE_IDS.actorInboxPropertyMismatch; +const config = { rule, ruleName }; + +runTests(ruleName, createMismatchRuleTests("inbox", config)); +runTests(ruleName, createMismatchEdgeCaseTests("inbox", config)); diff --git a/packages/lint/src/tests/actor-inbox-property-required.test.ts b/packages/lint/src/tests/actor-inbox-property-required.test.ts new file mode 100644 index 000000000..4f91fb021 --- /dev/null +++ b/packages/lint/src/tests/actor-inbox-property-required.test.ts @@ -0,0 +1,23 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createRequiredDispatcherRuleTests, + createRequiredEdgeCaseTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-inbox-property-required.ts"; + +const ruleName = RULE_IDS.actorInboxPropertyRequired; + +const config = { rule, ruleName }; + +// Standard required listener rule tests +runTests( + ruleName, + createRequiredDispatcherRuleTests("inbox", config), +); + +// Edge case tests +runTests( + ruleName, + createRequiredEdgeCaseTests("inbox", config), +); diff --git a/packages/lint/src/tests/actor-liked-property-mismatch.test.ts b/packages/lint/src/tests/actor-liked-property-mismatch.test.ts new file mode 100644 index 000000000..9631cf0b0 --- /dev/null +++ b/packages/lint/src/tests/actor-liked-property-mismatch.test.ts @@ -0,0 +1,13 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createMismatchEdgeCaseTests, + createMismatchRuleTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-liked-property-mismatch.ts"; + +const ruleName = RULE_IDS.actorLikedPropertyMismatch; +const config = { rule, ruleName }; + +runTests(ruleName, createMismatchRuleTests("liked", config)); +runTests(ruleName, createMismatchEdgeCaseTests("liked", config)); diff --git a/packages/lint/src/tests/actor-liked-property-required.test.ts b/packages/lint/src/tests/actor-liked-property-required.test.ts new file mode 100644 index 000000000..710874de6 --- /dev/null +++ b/packages/lint/src/tests/actor-liked-property-required.test.ts @@ -0,0 +1,16 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createRequiredDispatcherRuleTests, + createRequiredEdgeCaseTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-liked-property-required.ts"; + +const ruleName = RULE_IDS.actorLikedPropertyRequired; +const config = { rule, ruleName }; + +runTests(ruleName, createRequiredDispatcherRuleTests("liked", config)); +runTests( + ruleName, + createRequiredEdgeCaseTests("liked", config), +); diff --git a/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts new file mode 100644 index 000000000..690f24ee3 --- /dev/null +++ b/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts @@ -0,0 +1,13 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createMismatchEdgeCaseTests, + createMismatchRuleTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-outbox-property-mismatch.ts"; + +const ruleName = RULE_IDS.actorOutboxPropertyMismatch; +const config = { rule, ruleName }; + +runTests(ruleName, createMismatchRuleTests("outbox", config)); +runTests(ruleName, createMismatchEdgeCaseTests("outbox", config)); diff --git a/packages/lint/src/tests/actor-outbox-property-required.test.ts b/packages/lint/src/tests/actor-outbox-property-required.test.ts new file mode 100644 index 000000000..dc2f287c6 --- /dev/null +++ b/packages/lint/src/tests/actor-outbox-property-required.test.ts @@ -0,0 +1,16 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createRequiredDispatcherRuleTests, + createRequiredEdgeCaseTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-outbox-property-required.ts"; + +const ruleName = RULE_IDS.actorOutboxPropertyRequired; +const config = { rule, ruleName }; + +runTests(ruleName, createRequiredDispatcherRuleTests("outbox", config)); +runTests( + ruleName, + createRequiredEdgeCaseTests("outbox", config), +); diff --git a/packages/lint/src/tests/actor-public-key-required.test.ts b/packages/lint/src/tests/actor-public-key-required.test.ts new file mode 100644 index 000000000..be3b51261 --- /dev/null +++ b/packages/lint/src/tests/actor-public-key-required.test.ts @@ -0,0 +1,13 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createKeyRequiredDispatcherRuleTests, + createKeyRequiredEdgeCaseTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-public-key-required.ts"; + +const ruleName = RULE_IDS.actorPublicKeyRequired; +const config = { rule, ruleName }; + +runTests(ruleName, createKeyRequiredDispatcherRuleTests("publicKey", config)); +runTests(ruleName, createKeyRequiredEdgeCaseTests("publicKey", config)); diff --git a/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts new file mode 100644 index 000000000..ce961c65e --- /dev/null +++ b/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts @@ -0,0 +1,13 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createMismatchEdgeCaseTests, + createMismatchRuleTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-shared-inbox-property-mismatch.ts"; + +const ruleName = RULE_IDS.actorSharedInboxPropertyMismatch; +const config = { rule, ruleName }; + +runTests(ruleName, createMismatchRuleTests("sharedInbox", config)); +runTests(ruleName, createMismatchEdgeCaseTests("sharedInbox", config)); diff --git a/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts b/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts new file mode 100644 index 000000000..c45a5599e --- /dev/null +++ b/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts @@ -0,0 +1,23 @@ +import { RULE_IDS } from "../lib/const.ts"; +import { + createRequiredDispatcherRuleTests, + createRequiredEdgeCaseTests, + runTests, +} from "../lib/test-templates.ts"; +import * as rule from "../rules/actor-shared-inbox-property-required.ts"; + +const ruleName = RULE_IDS.actorSharedInboxPropertyRequired; + +const config = { rule, ruleName }; + +// Standard required listener rule tests +runTests( + ruleName, + createRequiredDispatcherRuleTests("sharedInbox", config), +); + +// Edge case tests +runTests( + ruleName, + createRequiredEdgeCaseTests("sharedInbox", config), +); diff --git a/packages/lint/src/tests/collection-filtering-not-implemented.test.ts b/packages/lint/src/tests/collection-filtering-not-implemented.test.ts new file mode 100644 index 000000000..7e57c10a6 --- /dev/null +++ b/packages/lint/src/tests/collection-filtering-not-implemented.test.ts @@ -0,0 +1,174 @@ +import { test } from "node:test"; +import { properties, RULE_IDS } from "../lib/const.ts"; +import { COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR as expectedError } from "../lib/messages.ts"; +import lintTest from "../lib/test.ts"; +import * as rule from "../rules/collection-filtering-not-implemented.ts"; + +const ruleName = RULE_IDS.collectionFilteringNotImplemented; +const filterless = ["ctx", "identifier", "cursor"] as const; + +// Helper to create test code for setFollowersDispatcher +const createFollowersDispatcherCode = ( + { + params = ["ctx", "identifier", "cursor", "filter"], + async = true, + arrow = true, + }: { + params?: readonly string[]; + async?: boolean; + arrow?: boolean; + } = {}, +): string => { + const paramsString = params.join(", "); + const asyncKeyword = async ? "async" : ""; + const [funcKeyword, arrowSymbol] = arrow ? ["", "=>"] : ["function", ""]; + + return ` + federation.setFollowersDispatcher( + "/users/{identifier}/followers", + ${asyncKeyword} ${funcKeyword}(${paramsString}) ${arrowSymbol} { + return { items: [] }; + } + ); + `; +}; + +test( + `${ruleName}: ✅ Good - async arrow function with filter parameter`, + lintTest({ + code: createFollowersDispatcherCode(), + rule, + ruleName, + }), +); + +test( + `${ruleName}: ✅ Good - async function expression with filter`, + lintTest({ + code: createFollowersDispatcherCode({ arrow: false }), + rule, + ruleName, + }), +); + +test( + `${ruleName}: ✅ Good - sync arrow function with filter`, + lintTest({ + code: createFollowersDispatcherCode({ async: false }), + rule, + ruleName, + }), +); + +test( + `${ruleName}: ✅ Good - sync function expression with filter`, + lintTest({ + code: createFollowersDispatcherCode({ async: false, arrow: false }), + rule, + ruleName, + }), +); + +test( + `${ruleName}: ❌ Bad - async arrow function without filter parameter`, + lintTest({ + code: createFollowersDispatcherCode({ params: filterless }), + rule, + ruleName, + expectedError, + }), +); + +test( + `${ruleName}: ❌ Bad - async function expression without filter`, + lintTest({ + code: createFollowersDispatcherCode({ params: filterless, arrow: false }), + rule, + ruleName, + expectedError, + }), +); + +test( + `${ruleName}: ❌ Bad - sync arrow function expression without filter`, + lintTest({ + code: createFollowersDispatcherCode({ params: filterless, async: false }), + rule, + ruleName, + expectedError, + }), +); + +test( + `${ruleName}: ❌ Bad - sync function expression without filter`, + lintTest({ + code: createFollowersDispatcherCode({ + params: filterless, + async: false, + arrow: false, + }), + rule, + ruleName, + expectedError, + }), +); + +test( + `${ruleName}: ✅ Good - 4th parameter but unnamed filter`, + lintTest({ + code: createFollowersDispatcherCode({ + params: ["ctx", "identifier", "cursor", "baseUri"], + }), + rule, + ruleName, + }), +); + +test( + `${ruleName}: ❌ Bad - only two parameters (missing cursor and filter)`, + lintTest({ + code: createFollowersDispatcherCode({ params: ["ctx", "identifier"] }), + rule, + ruleName, + expectedError, + }), +); + +test( + `${ruleName}: ✅ Good - non-federation object is not checked`, + lintTest({ + code: createFollowersDispatcherCode({ params: filterless }), + rule, + ruleName, + federationSetup: ` + const federation = { + setFollowersDispatcher: () => {} + }; + `, + }), +); + +// Test that other collection dispatchers are NOT checked +const otherDispatchers = [ + "following", + "outbox", + "liked", + "featured", + "featuredTags", +] as const satisfies (keyof typeof properties)[]; + +const paramsString = filterless.join(", "); + +test( + `${ruleName}: ✅ Good - other collection dispatchers without filter are NOT checked`, + lintTest({ + code: otherDispatchers.map((name) => + `federation.${properties[name].setter}( + "/users/{identifier}/${name}", + async (${paramsString}) => { items: [] }, + );` + ).join("\n"), + rule, + ruleName, + }), +); diff --git a/packages/lint/src/tests/integration.test.ts b/packages/lint/src/tests/integration.test.ts new file mode 100644 index 000000000..3675d5e63 --- /dev/null +++ b/packages/lint/src/tests/integration.test.ts @@ -0,0 +1,752 @@ +/** + * Integration tests for all Fedify lint rules. + * Based on the example code in examples/lint/deno/mod.ts + * + * All tests start from the complete valid code and modify only the part + * necessary to trigger the specific lint rule being tested. + */ + +import { map, pipe, prop, toArray } from "@fxts/core"; +import * as parser from "@typescript-eslint/parser"; +import { Linter } from "eslint"; +import { equal, ok } from "node:assert/strict"; +import { test } from "node:test"; +import { plugin as eslintPlugin } from "../index.ts"; +import { replace } from "../lib/utils.ts"; +import denoPlugin from "../mod.ts"; + +const PLUGIN_NAME = "Deno" in globalThis ? "fedify-lint" : "@fedify/lint"; + +type Diagnostic = { + id: string; + message: string; +}; + +/** + * Run all lint rules on the given code and return diagnostics. + */ +const lintTest = (code: string): Diagnostic[] => + "Deno" in globalThis ? testDenoLint(code) : testEslint(code); + +const testDenoLint = (code: string) => + Deno.lint.runPlugin( + denoPlugin, + "integration.test.ts", + code, + ) as Diagnostic[]; + +function testEslint(code: string) { + // For Node.js environment using ESLint flat config + const linter = new Linter({ configType: "flat" }); + + const config = [{ + files: ["**/*.ts"], + plugins: { "@fedify/lint": eslintPlugin }, + rules: eslintPlugin.configs.recommended.rules, + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + parser, + }, + }]; + + const results = linter.verify(code, config, "integration.test.ts"); + + return results.map((msg) => ({ + id: msg.ruleId ?? "unknown", + message: msg.message, + })); +} + +/** + * Assert that the code passes all lint rules (no diagnostics). + */ +function assertNoErrors(code: string) { + const diagnostics = lintTest(code); + equal( + diagnostics.length, + 0, + `Expected no errors but got: ${ + diagnostics.map((d) => `${d.id}: ${d.message}`).join(", ") + }`, + ); +} + +/** + * Assert that the code has exactly one error matching the given rule. + */ +const assertHasError = (ruleName: string) => (code: string) => { + const diagnostics = lintTest(code); + const ruleId = `${PLUGIN_NAME}/${ruleName}`; + const matched = diagnostics.some((d) => d.id === ruleId); + ok( + matched, + `Expected error from ${ruleName} but got: ${ + diagnostics.length === 0 + ? "no errors" + : diagnostics.map((d) => d.id).join(", ") + }`, + ); +}; + +/** + * Complete valid code that passes all lint rules. + * This is the baseline for all tests - each test modifies only what's needed. + */ +const COMPLETE_VALID_CODE = ` +import { + createFederation, + Endpoints, + InProcessMessageQueue, + MemoryKvStore, + Person, +} from "@fedify/fedify"; + +const federation = createFederation({ + kv: new MemoryKvStore(), + queue: new InProcessMessageQueue(), +}); + +federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const keyPairs = await ctx.getActorKeyPairs(identifier); + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + summary: "A test actor for comprehensive lint rule validation", + inbox: ctx.getInboxUri(identifier), + endpoints: new Endpoints({ + sharedInbox: ctx.getInboxUri(), + }), + outbox: ctx.getOutboxUri(identifier), + following: ctx.getFollowingUri(identifier), + followers: ctx.getFollowersUri(identifier), + liked: ctx.getLikedUri(identifier), + featured: ctx.getFeaturedUri(identifier), + featuredTags: ctx.getFeaturedTagsUri(identifier), + publicKey: keyPairs[0]?.cryptographicKey, + assertionMethod: keyPairs[0]?.multikey, + }); + }) + .setKeyPairsDispatcher(async (_ctx, _identifier) => []); + +federation.setInboxListeners("/users/{identifier}/inbox", "/inbox"); + +federation.setOutboxDispatcher( + "/users/{identifier}/outbox", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +); + +federation.setFollowingDispatcher( + "/users/{identifier}/following", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +); + +federation.setFollowersDispatcher( + "/users/{identifier}/followers", + async (_ctx, _identifier, _cursor, _filter) => { + return { items: [] }; + }, +); + +federation.setLikedDispatcher( + "/users/{identifier}/liked", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +); + +federation.setFeaturedDispatcher( + "/users/{identifier}/featured", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +); + +federation.setFeaturedTagsDispatcher( + "/users/{identifier}/tags", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +); +`; + +test("Integration: ✅ Complete valid code passes all rules", () => { + assertNoErrors(COMPLETE_VALID_CODE); +}); + +test("Integration: ❌ actor-id-required - missing id property", () => + pipe( + COMPLETE_VALID_CODE, + replace( + "id: ctx.getActorUri(identifier),", + "// id: ctx.getActorUri(identifier), // REMOVED", + ), + assertHasError("actor-id-required"), + )); + +test( + "Integration: ❌ actor-inbox-property-required - missing inbox property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "inbox: ctx.getInboxUri(identifier),", + "// inbox: ctx.getInboxUri(identifier), // REMOVED", + ), + assertHasError("actor-inbox-property-required"), + ), +); + +test( + "Integration: ❌ actor-shared-inbox-property-required \ +- missing sharedInbox property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + `endpoints: new Endpoints({ + sharedInbox: ctx.getInboxUri(), + }),`, + "// endpoints: REMOVED", + ), + assertHasError("actor-shared-inbox-property-required"), + ), +); + +test( + "Integration: ❌ actor-following-property-required \ +- missing following property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "following: ctx.getFollowingUri(identifier),", + "// following: ctx.getFollowingUri(identifier), // REMOVED", + ), + assertHasError("actor-following-property-required"), + ), +); + +test( + "Integration: ❌ actor-followers-property-required \ +- missing followers property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "followers: ctx.getFollowersUri(identifier),", + "// followers: ctx.getFollowersUri(identifier), // REMOVED", + ), + assertHasError("actor-followers-property-required"), + ), +); + +test( + "Integration: ❌ actor-outbox-property-required \ +- missing outbox property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "outbox: ctx.getOutboxUri(identifier),", + "// outbox: ctx.getOutboxUri(identifier), // REMOVED", + ), + assertHasError("actor-outbox-property-required"), + ), +); + +test( + "Integration: ❌ actor-liked-property-required \ +- missing liked property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "liked: ctx.getLikedUri(identifier),", + "// liked: ctx.getLikedUri(identifier), // REMOVED", + ), + assertHasError("actor-liked-property-required"), + ), +); + +test( + "Integration: ❌ actor-featured-property-required \ +- missing featured property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "featured: ctx.getFeaturedUri(identifier),", + "// featured: ctx.getFeaturedUri(identifier), // REMOVED", + ), + assertHasError("actor-featured-property-required"), + ), +); + +test( + "Integration: ❌ actor-featured-tags-property-required \ +- missing featuredTags property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "featuredTags: ctx.getFeaturedTagsUri(identifier),", + "// featuredTags: ctx.getFeaturedTagsUri(identifier), // REMOVED", + ), + assertHasError("actor-featured-tags-property-required"), + ), +); + +test( + "Integration: ❌ actor-public-key-required \ +- missing publicKey property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "publicKey: keyPairs[0]?.cryptographicKey,", + "// publicKey: keyPairs[0]?.cryptographicKey, // REMOVED", + ), + assertHasError("actor-public-key-required"), + ), +); + +test( + "Integration: ❌ actor-assertion-method-required \ +- missing assertionMethod property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "assertionMethod: keyPairs[0]?.multikey,", + "// assertionMethod: keyPairs[0]?.multikey, // REMOVED", + ), + assertHasError("actor-assertion-method-required"), + ), +); + +// ============================================================================= +// Test: *-mismatch rules (property uses wrong context method) +// ============================================================================= + +test("Integration: ❌ actor-id-mismatch - id uses wrong context method", () => + pipe( + COMPLETE_VALID_CODE, + replace( + "id: ctx.getActorUri(identifier),", + "id: ctx.getInboxUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-id-mismatch"), + )); + +test( + "Integration: ❌ actor-inbox-property-mismatch \ +- inbox uses wrong context method", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "inbox: ctx.getInboxUri(identifier),", + "inbox: ctx.getOutboxUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-inbox-property-mismatch"), + ), +); + +test( + "Integration: ❌ actor-shared-inbox-property-mismatch \ +- sharedInbox uses wrong context method", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "sharedInbox: ctx.getInboxUri(),", + "sharedInbox: ctx.getOutboxUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-shared-inbox-property-mismatch"), + ), +); + +test( + "Integration: ❌ actor-following-property-mismatch \ +- following uses wrong context method", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "following: ctx.getFollowingUri(identifier),", + "following: ctx.getFollowersUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-following-property-mismatch"), + ), +); + +test( + "Integration: ❌ actor-followers-property-mismatch \ +- followers uses wrong context method", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "followers: ctx.getFollowersUri(identifier),", + "followers: ctx.getFollowingUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-followers-property-mismatch"), + ), +); + +test( + "Integration: ❌ actor-outbox-property-mismatch \ +- outbox uses wrong context method", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "outbox: ctx.getOutboxUri(identifier),", + "outbox: ctx.getInboxUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-outbox-property-mismatch"), + ), +); + +test( + "Integration: ❌ actor-liked-property-mismatch \ +- liked uses wrong context method", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "liked: ctx.getLikedUri(identifier),", + "liked: ctx.getOutboxUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-liked-property-mismatch"), + ), +); + +test( + "Integration: ❌ actor-featured-property-mismatch \ +- featured uses wrong context method", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "featured: ctx.getFeaturedUri(identifier),", + "featured: ctx.getOutboxUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-featured-property-mismatch"), + ), +); + +test( + "Integration: ❌ actor-featured-tags-property-mismatch \ +- featuredTags uses wrong context method", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "featuredTags: ctx.getFeaturedTagsUri(identifier),", + "featuredTags: ctx.getOutboxUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-featured-tags-property-mismatch"), + ), +); + +// ============================================================================= +// Test: collection-filtering-not-implemented +// ============================================================================= + +test( + "Integration: ❌ collection-filtering-not-implemented \ +- setFollowersDispatcher without filter parameter", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "async (_ctx, _identifier, _cursor, _filter) => {", + "async (_ctx, _identifier, _cursor) => { // NO FILTER", + ), + assertHasError("collection-filtering-not-implemented"), + ), +); + +// ============================================================================= +// Test: Non-Federation objects should not trigger errors +// ============================================================================= + +test("Integration: ✅ Non-Federation object - custom federation object", () => + pipe( + COMPLETE_VALID_CODE, + replace( + `const federation = createFederation({ + kv: new MemoryKvStore(), + queue: new InProcessMessageQueue(), +});`, + `const federation = { + setActorDispatcher: () => ({ setKeyPairsDispatcher: () => {} }), + setInboxListeners: () => {}, + setOutboxDispatcher: () => {}, + setFollowingDispatcher: () => {}, + setFollowersDispatcher: () => {}, + setLikedDispatcher: () => {}, + setFeaturedDispatcher: () => {}, + setFeaturedTagsDispatcher: () => {}, +};`, + ), + assertNoErrors, + )); + +// ============================================================================= +// Test: Dispatcher not configured - property not required +// ============================================================================= + +test( + "Integration: ✅ No setFollowingDispatcher - following property not required", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + ` outbox: ctx.getOutboxUri(identifier), + following: ctx.getFollowingUri(identifier), + followers: ctx.getFollowersUri(identifier),`, + ` outbox: ctx.getOutboxUri(identifier), + followers: ctx.getFollowersUri(identifier),`, + ), + replace( + `federation.setFollowingDispatcher( + "/users/{identifier}/following", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +); + +`, + "", + ), + assertNoErrors, + ), +); + +test( + "Integration: ✅ No setFollowersDispatcher - followers property not required", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + ` following: ctx.getFollowingUri(identifier), + followers: ctx.getFollowersUri(identifier), + liked: ctx.getLikedUri(identifier),`, + ` following: ctx.getFollowingUri(identifier), + liked: ctx.getLikedUri(identifier),`, + ), + replace( + `federation.setFollowersDispatcher( + "/users/{identifier}/followers", + async (_ctx, _identifier, _cursor, _filter) => { + return { items: [] }; + }, +); + +`, + "", + ), + assertNoErrors, + ), +); + +test( + "Integration: ✅ No setOutboxDispatcher - outbox property not required", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + ` }), + outbox: ctx.getOutboxUri(identifier), + following: ctx.getFollowingUri(identifier),`, + ` }), + following: ctx.getFollowingUri(identifier),`, + ), + replace( + `federation.setOutboxDispatcher( + "/users/{identifier}/outbox", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +); + +`, + "", + ), + assertNoErrors, + ), +); + +test( + "Integration: ✅ No setLikedDispatcher - liked property not required", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + ` followers: ctx.getFollowersUri(identifier), + liked: ctx.getLikedUri(identifier), + featured: ctx.getFeaturedUri(identifier),`, + ` followers: ctx.getFollowersUri(identifier), + featured: ctx.getFeaturedUri(identifier),`, + ), + replace( + `federation.setLikedDispatcher( + "/users/{identifier}/liked", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +); + +`, + "", + ), + assertNoErrors, + ), +); + +test( + "Integration: ✅ No setFeaturedDispatcher - featured property not required", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + ` liked: ctx.getLikedUri(identifier), + featured: ctx.getFeaturedUri(identifier), + featuredTags: ctx.getFeaturedTagsUri(identifier),`, + ` liked: ctx.getLikedUri(identifier), + featuredTags: ctx.getFeaturedTagsUri(identifier),`, + ), + replace( + `federation.setFeaturedDispatcher( + "/users/{identifier}/featured", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +); + +`, + "", + ), + assertNoErrors, + ), +); + +test( + "Integration: ✅ No setFeaturedTagsDispatcher \ +- featuredTags property not required", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + ` featured: ctx.getFeaturedUri(identifier), + featuredTags: ctx.getFeaturedTagsUri(identifier), + publicKey: keyPairs[0]?.cryptographicKey,`, + ` featured: ctx.getFeaturedUri(identifier), + publicKey: keyPairs[0]?.cryptographicKey,`, + ), + replace( + `federation.setFeaturedTagsDispatcher( + "/users/{identifier}/tags", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +); +`, + "", + ), + assertNoErrors, + ), +); + +test( + "Integration: ✅ No setInboxListeners - inbox/sharedInbox not required", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + ` summary: "A test actor for comprehensive lint rule validation", + inbox: ctx.getInboxUri(identifier), + endpoints: new Endpoints({ + sharedInbox: ctx.getInboxUri(), + }), + outbox: ctx.getOutboxUri(identifier),`, + ` summary: "A test actor for comprehensive lint rule validation", + outbox: ctx.getOutboxUri(identifier),`, + ), + replace( + `federation.setInboxListeners("/users/{identifier}/inbox", "/inbox"); + +`, + "", + ), + assertNoErrors, + ), +); + +test( + "Integration: ✅ No setKeyPairsDispatcher \ +- publicKey/assertionMethod not required", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + ` .setActorDispatcher("/users/{identifier}", \ +async (ctx, identifier) => { + const keyPairs = await ctx.getActorKeyPairs(identifier); + return new Person({`, + ` .setActorDispatcher("/users/{identifier}", \ +async (ctx, identifier) => { + return new Person({`, + ), + replace( + ` featuredTags: ctx.getFeaturedTagsUri(identifier), + publicKey: keyPairs[0]?.cryptographicKey, + assertionMethod: keyPairs[0]?.multikey, + });`, + ` featuredTags: ctx.getFeaturedTagsUri(identifier), + });`, + ), + replace( + ` }) + .setKeyPairsDispatcher(async (_ctx, _identifier) => []); + +federation.setInboxListeners`, + ` }); + +federation.setInboxListeners`, + ), + assertNoErrors, + ), +); + +// ============================================================================= +// Test: Multiple errors in one file +// ============================================================================= + +test("Integration: ❌ Multiple errors - missing id and inbox", () => + pipe( + COMPLETE_VALID_CODE, + replace("id: ctx.getActorUri(identifier),", "// id: REMOVED"), + replace("inbox: ctx.getInboxUri(identifier),", "// inbox: REMOVED"), + lintTest, + map(prop("id")), + toArray, + (ids) => + [ + "actor-id-required", + "actor-inbox-property-required", + ].forEach((ruleName) => + ok( + ids.includes(`${PLUGIN_NAME}/${ruleName}`), + `Expected ${ruleName} error`, + ) + ), + )); diff --git a/packages/lint/tsdown.config.ts b/packages/lint/tsdown.config.ts new file mode 100644 index 000000000..70a57795c --- /dev/null +++ b/packages/lint/tsdown.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: ["src/index.ts"], + dts: true, + format: ["esm", "cjs"], + platform: "node", +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb7a514c0..246666400 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ catalogs: specifier: ^4.20250529.0 version: 4.20250529.0 '@fxts/core': - specifier: ^1.15.0 + specifier: ^1.20.0 version: 1.20.0 '@js-temporal/polyfill': specifier: ^0.5.1 @@ -64,8 +64,8 @@ catalogs: specifier: ^1.3.8 version: 1.3.8 es-toolkit: - specifier: 1.39.10 - version: 1.39.10 + specifier: 1.42.0 + version: 1.42.0 express: specifier: ^4.0.0 version: 4.21.2 @@ -147,6 +147,9 @@ importers: '@fedify/koa': specifier: 'workspace:' version: link:../packages/koa + '@fedify/lint': + specifier: 'workspace:' + version: link:../packages/lint '@fedify/nestjs': specifier: 'workspace:' version: link:../packages/nestjs @@ -715,7 +718,7 @@ importers: version: 3.0.0 es-toolkit: specifier: 'catalog:' - version: 1.39.10 + version: 1.42.0 fetch-mock: specifier: 'catalog:' version: 12.6.0 @@ -968,6 +971,37 @@ importers: specifier: 'catalog:' version: 5.9.3 + packages/lint: + dependencies: + '@fedify/fedify': + specifier: workspace:^ + version: link:../fedify + '@fxts/core': + specifier: 'catalog:' + version: 1.20.0 + '@typescript-eslint/parser': + specifier: ^8.49.0 + version: 8.50.0(eslint@9.32.0(jiti@2.5.1))(typescript@5.9.3) + '@typescript-eslint/utils': + specifier: ^8.0.0 + version: 8.41.0(eslint@9.32.0(jiti@2.5.1))(typescript@5.9.3) + devDependencies: + '@types/eslint': + specifier: ^9.0.0 + version: 9.6.1 + '@types/estree': + specifier: ^1.0.8 + version: 1.0.8 + eslint: + specifier: ^9.0.0 + version: 9.32.0(jiti@2.5.1) + tsdown: + specifier: 'catalog:' + version: 0.12.9(typescript@5.9.3) + typescript: + specifier: 'catalog:' + version: 5.9.3 + packages/nestjs: dependencies: '@fedify/fedify': @@ -3869,6 +3903,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -4053,6 +4090,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser@8.50.0': + resolution: {integrity: sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.38.0': resolution: {integrity: sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4065,6 +4109,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.50.0': + resolution: {integrity: sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/scope-manager@7.18.0': resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -4077,6 +4127,10 @@ packages: resolution: {integrity: sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.50.0': + resolution: {integrity: sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.38.0': resolution: {integrity: sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4089,6 +4143,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/tsconfig-utils@8.50.0': + resolution: {integrity: sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@7.18.0': resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -4125,6 +4185,10 @@ packages: resolution: {integrity: sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.50.0': + resolution: {integrity: sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@7.18.0': resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -4146,6 +4210,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/typescript-estree@8.50.0': + resolution: {integrity: sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@7.18.0': resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} engines: {node: ^18.18.0 || >=20.0.0} @@ -4178,6 +4248,10 @@ packages: resolution: {integrity: sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.50.0': + resolution: {integrity: sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/vfs@1.6.1': resolution: {integrity: sha512-JwoxboBh7Oz1v38tPbkrZ62ZXNHAk9bJ7c9x0eI5zBfBnBYGhURdbnh7Z4smN/MV48Y5OCcZb58n972UtbazsA==} peerDependencies: @@ -5291,9 +5365,6 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - es-toolkit@1.39.10: - resolution: {integrity: sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==} - es-toolkit@1.39.5: resolution: {integrity: sha512-z9V0qU4lx1TBXDNFWfAASWk6RNU6c6+TJBKE+FLIg8u0XJ6Yw58Hi0yX8ftEouj6p1QARRlXLFfHbIli93BdQQ==} @@ -6141,10 +6212,6 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true - jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} - hasBin: true - jiti@2.5.1: resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} hasBin: true @@ -7883,6 +7950,10 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + to-data-view@1.1.0: resolution: {integrity: sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==} @@ -10777,7 +10848,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 enhanced-resolve: 5.18.2 - jiti: 2.4.2 + jiti: 2.5.1 lightningcss: 1.30.1 magic-string: 0.30.17 source-map-js: 1.2.1 @@ -11094,6 +11165,11 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@types/estree@1.0.8': {} '@types/express-serve-static-core@4.19.6': @@ -11363,6 +11439,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.50.0(eslint@9.32.0(jiti@2.5.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.0 + debug: 4.4.1 + eslint: 9.32.0(jiti@2.5.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/project-service@8.38.0(typescript@5.9.2)': dependencies: '@typescript-eslint/tsconfig-utils': 8.41.0(typescript@5.9.2) @@ -11390,6 +11478,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.41.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.41.0(typescript@5.9.3) + '@typescript-eslint/types': 8.41.0 + debug: 4.4.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.50.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) + '@typescript-eslint/types': 8.50.0 + debug: 4.4.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@7.18.0': dependencies: '@typescript-eslint/types': 7.18.0 @@ -11405,6 +11511,11 @@ snapshots: '@typescript-eslint/types': 8.41.0 '@typescript-eslint/visitor-keys': 8.41.0 + '@typescript-eslint/scope-manager@8.50.0': + dependencies: + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/visitor-keys': 8.50.0 + '@typescript-eslint/tsconfig-utils@8.38.0(typescript@5.9.2)': dependencies: typescript: 5.9.2 @@ -11421,6 +11532,10 @@ snapshots: dependencies: typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.50.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) @@ -11475,6 +11590,8 @@ snapshots: '@typescript-eslint/types@8.41.0': {} + '@typescript-eslint/types@8.50.0': {} + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 7.18.0 @@ -11538,6 +11655,37 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.41.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.41.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.41.0(typescript@5.9.3) + '@typescript-eslint/types': 8.41.0 + '@typescript-eslint/visitor-keys': 8.41.0 + debug: 4.4.1 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.50.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.50.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/visitor-keys': 8.50.0 + debug: 4.4.1 + minimatch: 9.0.5 + semver: 7.7.2 + tinyglobby: 0.2.15 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) @@ -11582,6 +11730,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.41.0(eslint@9.32.0(jiti@2.5.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.32.0(jiti@2.5.1)) + '@typescript-eslint/scope-manager': 8.41.0 + '@typescript-eslint/types': 8.41.0 + '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.9.3) + eslint: 9.32.0(jiti@2.5.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@7.18.0': dependencies: '@typescript-eslint/types': 7.18.0 @@ -11597,6 +11756,11 @@ snapshots: '@typescript-eslint/types': 8.41.0 eslint-visitor-keys: 4.2.1 + '@typescript-eslint/visitor-keys@8.50.0': + dependencies: + '@typescript-eslint/types': 8.50.0 + eslint-visitor-keys: 4.2.1 + '@typescript/vfs@1.6.1(typescript@5.9.3)': dependencies: debug: 4.4.1 @@ -12753,8 +12917,6 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - es-toolkit@1.39.10: {} - es-toolkit@1.39.5: {} es-toolkit@1.42.0: {} @@ -14067,8 +14229,6 @@ snapshots: jiti@1.21.7: {} - jiti@2.4.2: {} - jiti@2.5.1: {} jpeg-js@0.4.4: {} @@ -16076,6 +16236,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + to-data-view@1.1.0: {} to-data-view@2.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b82a3bc53..51147c041 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,9 +7,9 @@ packages: - packages/fastify - packages/fedify - packages/h3 -- packages/h3 - packages/hono - packages/koa +- packages/lint - packages/nestjs - packages/next - packages/postgres @@ -33,7 +33,7 @@ packages: catalog: "@cloudflare/workers-types": ^4.20250529.0 - "@fxts/core": ^1.15.0 + "@fxts/core": ^1.20.0 "@js-temporal/polyfill": ^0.5.1 "@logtape/file": ^1.2.2 "@logtape/logtape": ^1.2.2 @@ -51,7 +51,7 @@ catalog: amqplib: ^0.10.8 byte-encodings: ^1.0.11 elysia: ^1.3.8 - es-toolkit: 1.39.10 + es-toolkit: 1.42.0 express: ^4.0.0 fastify: ^5.2.0 fastify-plugin: ^5.0.1