diff --git a/.env.example b/.env.example index 464f28bb..0d64fcba 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,6 @@ MANAGER_ASSET_REGEX=apk$ MANAGER_DOWNLOADERS_REPO=revanced-manager-downloaders MANAGER_DOWNLOADERS_ASSET_REGEX=apk$ -CONTRIBUTORS_REPOS=revanced-patcher:ReVanced Patcher,revanced-patches:ReVanced Patches,revanced-website:ReVanced Website,revanced-cli:ReVanced CLI,revanced-manager:ReVanced Manager +CONTRIBUTORS_REPOS=revanced-manager:ReVanced Manager,revanced-patches:ReVanced Patches,revanced-cli:ReVanced CLI,revanced-documentation:ReVanced Documentation,revanced-website:ReVanced Website,revanced-patcher:ReVanced Patcher,revanced-api:ReVanced API,revanced-manager-downloaders:ReVanced Manager Downloaders,revanced-bots:ReVanced Bots,revanced-library:ReVanced Library,revanced-patches-gradle-plugin:ReVanced Patches Gradle Plugin,revanced-patches-template:ReVanced Patches Template,revanced-cloudflare-email-worker:ReVanced Cloudflare Email Worker,revanced-encrypt:ReVanced Encrypt,revanced-vote:ReVanced Vote,revanced-invoice:ReVanced Invoice,revanced-manager-downloaders-template:ReVanced Manager Downloaders Template,revanced-branding:ReVanced Branding API_VERSION=5 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 8bc7f7ee..6a7685b9 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,4 +2,4 @@ blank_issues_enabled: false contact_links: - name: 🗨 Discussions url: https://github.com/revanced/revanced-suggestions/discussions - about: Have something unspecific to ReVanced APi in mind? Search for or start a new discussion! + about: Have something unspecific to ReVanced API in mind? Search for or start a new discussion! diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index fab7f3fa..d2150183 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -13,7 +13,7 @@ body: media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-api/main/assets/revanced-headline/revanced-headline-vertical-dark.svg" > - @@ -66,7 +66,7 @@ body: Continuing the legacy of Vanced

- # ReVanced APi feature request + # ReVanced API feature request Before creating a new feature request, please keep the following in mind: @@ -82,10 +82,10 @@ body: - type: textarea attributes: label: Motivation - description: | + description: | A strong motivation is necessary for a feature request to be considered. - - - Why should this feature be implemented? + + - Why should this feature be implemented? - What is the explicit use case? - What are the benefits? - What makes this feature important? diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..9b2d3386 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +.github/ +drizzle/ +bun.lock +node_modules/ +*.md \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..867d92e9 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "tabWidth": 4 +} diff --git a/about.example.json b/about.example.json index 8bbaf4c9..cf59fbe6 100644 --- a/about.example.json +++ b/about.example.json @@ -1,85 +1,85 @@ { - "name": "ReVanced", - "about": "ReVanced was born out of Vanced's discontinuation and it is our goal to continue the legacy of what Vanced left behind. Thanks to ReVanced Patcher, it's possible to create long-lasting patches for nearly any Android app. ReVanced's patching system is designed to allow patches to work on new versions of the apps automatically with bare minimum maintenance.", - "keys": "https://api.revanced.app/keys", - "branding": { - "logo": "https://raw.githubusercontent.com/ReVanced/revanced-branding/main/assets/revanced-logo/revanced-logo.svg" - }, - "status": "https://status.revanced.app", - "contact": { - "email": "contact@revanced.app" - }, - "socials": [ - { - "name": "Website", - "url": "https://revanced.app", - "preferred": true + "name": "ReVanced", + "about": "ReVanced was born out of Vanced's discontinuation and it is our goal to continue the legacy of what Vanced left behind. Thanks to ReVanced Patcher, it's possible to create long-lasting patches for nearly any Android app. ReVanced's patching system is designed to allow patches to work on new versions of the apps automatically with bare minimum maintenance.", + "keys": "https://api.revanced.app/keys", + "branding": { + "logo": "https://raw.githubusercontent.com/ReVanced/revanced-branding/main/assets/revanced-logo/revanced-logo.svg" }, - { - "name": "GitHub", - "url": "https://github.com/revanced" + "status": "https://status.revanced.app", + "contact": { + "email": "contact@revanced.app" }, - { - "name": "Twitter", - "url": "https://twitter.com/revancedapp" - }, - { - "name": "Discord", - "url": "https://revanced.app/discord", - "preferred": true - }, - { - "name": "Reddit", - "url": "https://www.reddit.com/r/revancedapp" - }, - { - "name": "Telegram", - "url": "https://t.me/app_revanced" - }, - { - "name": "YouTube", - "url": "https://www.youtube.com/@ReVanced" - } - ], - "donations": { - "wallets": [ - { - "network": "Bitcoin", - "currency_code": "BTC", - "address": "bc1q4x8j6mt27y5gv0q625t8wkr87ruy8fprpy4v3f" - }, - { - "network": "Dogecoin", - "currency_code": "DOGE", - "address": "D8GH73rNjudgi6bS2krrXWEsU9KShedLXp", - "preferred": true - }, - { - "network": "Ethereum", - "currency_code": "ETH", - "address": "0x7ab4091e00363654bf84B34151225742cd92FCE5" - }, - { - "network": "Litecoin", - "currency_code": "LTC", - "address": "LbJi8EuoDcwaZvykcKmcrM74jpjde23qJ2" - }, - { - "network": "Monero", - "currency_code": "XMR", - "address": "46YwWDbZD6jVptuk5mLHsuAmh1BnUMSjSNYacozQQEraWSQ93nb2yYVRHoMR6PmFYWEHsLHg9tr1cH5M8Rtn7YaaGQPCjSh" - } + "socials": [ + { + "name": "Website", + "url": "https://revanced.app", + "preferred": true + }, + { + "name": "GitHub", + "url": "https://github.com/revanced" + }, + { + "name": "Twitter", + "url": "https://twitter.com/revancedapp" + }, + { + "name": "Discord", + "url": "https://revanced.app/discord", + "preferred": true + }, + { + "name": "Reddit", + "url": "https://www.reddit.com/r/revancedapp" + }, + { + "name": "Telegram", + "url": "https://t.me/app_revanced" + }, + { + "name": "YouTube", + "url": "https://www.youtube.com/@ReVanced" + } ], - "links": [ - { - "name": "Open Collective", - "url": "https://opencollective.com/revanced", - "preferred": true - }, - { - "name": "GitHub Sponsors", - "url": "https://github.com/sponsors/ReVanced" - } - ] - } -} \ No newline at end of file + "donations": { + "wallets": [ + { + "network": "Bitcoin", + "currency_code": "BTC", + "address": "bc1q4x8j6mt27y5gv0q625t8wkr87ruy8fprpy4v3f" + }, + { + "network": "Dogecoin", + "currency_code": "DOGE", + "address": "D8GH73rNjudgi6bS2krrXWEsU9KShedLXp", + "preferred": true + }, + { + "network": "Ethereum", + "currency_code": "ETH", + "address": "0x7ab4091e00363654bf84B34151225742cd92FCE5" + }, + { + "network": "Litecoin", + "currency_code": "LTC", + "address": "LbJi8EuoDcwaZvykcKmcrM74jpjde23qJ2" + }, + { + "network": "Monero", + "currency_code": "XMR", + "address": "46YwWDbZD6jVptuk5mLHsuAmh1BnUMSjSNYacozQQEraWSQ93nb2yYVRHoMR6PmFYWEHsLHg9tr1cH5M8Rtn7YaaGQPCjSh" + } + ], + "links": [ + { + "name": "Open Collective", + "url": "https://opencollective.com/revanced", + "preferred": true + }, + { + "name": "GitHub Sponsors", + "url": "https://github.com/sponsors/ReVanced" + } + ] + } +} diff --git a/bun.lock b/bun.lock index f1135e22..ba8ee245 100644 --- a/bun.lock +++ b/bun.lock @@ -5,23 +5,24 @@ "": { "name": "revanced-api", "dependencies": { - "@hono/swagger-ui": "^0.6.0", + "@hono/swagger-ui": "^0.6.1", "@hono/zod-openapi": "^1.2.2", "drizzle-orm": "^0.45.1", - "hono": "^4.12.5", + "hono": "^4.12.8", "zod": "^4.3.6", }, "devDependencies": { - "@cloudflare/workers-types": "^4.20260307.1", - "@kilianpaquier/semantic-release-backmerge": "^1.7.1", + "@cloudflare/workers-types": "^4.20260317.1", + "@kilianpaquier/semantic-release-backmerge": "^1.7.3", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/npm": "^13.1.5", - "@types/node": "^25.3.5", - "drizzle-kit": "^0.31.9", + "@types/node": "^25.5.0", + "drizzle-kit": "^0.31.10", + "prettier": "3.8.1", "semantic-release": "^25.0.3", "typescript": "^5.9.3", - "wrangler": "^4.71.0", + "wrangler": "^4.76.0", }, }, }, @@ -42,19 +43,19 @@ "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="], - "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.15.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-EGYmJaGZKWl+X8tXxcnx4v2bOZSjQeNI5dWFeXivgX9+YCT69AkzHHwlNbVpqtEUTbew8eQurpyOpeN8fg00nw=="], + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg=="], - "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260312.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-HUAtDWaqUduS6yasV6+NgsK7qBpP1qGU49ow/Wb117IHjYp+PZPUGReDYocpB4GOMRoQlvdd4L487iFxzdARpw=="], + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260317.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-8hjh3sPMwY8M/zedq3/sXoA2Q4BedlGufn3KOOleIG+5a4ReQKLlUah140D7J6zlKmYZAFMJ4tWC7hCuI/s79g=="], - "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260312.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DOn7TPTHSxJYfi4m4NYga/j32wOTqvJf/pY4Txz5SDKWIZHSTXFyGz2K4B+thoPWLop/KZxGoyTv7db0mk/qyw=="], + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260317.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M/MnNyvO5HMgoIdr3QHjdCj2T1ki9gt0vIUnxYxBu9ISXS/jgtMl6chUVPJ7zHYBn9MyYr8ByeN6frjYxj0MGg=="], - "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260312.1", "", { "os": "linux", "cpu": "x64" }, "sha512-TdkIh3WzPXYHuvz7phAtFEEvAxvFd30tHrm4gsgpw0R0F5b8PtoM3hfL2uY7EcBBWVYUBtkY2ahDYFfufnXw/g=="], + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260317.1", "", { "os": "linux", "cpu": "x64" }, "sha512-1ltuEjkRcS3fsVF7CxsKlWiRmzq2ZqMfqDN0qUOgbUwkpXsLVJsXmoblaLf5OP00ELlcgF0QsN0p2xPEua4Uug=="], - "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260312.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-kNauZhL569Iy94t844OMwa1zP6zKFiL3xiJ4tGLS+TFTEfZ3pZsRH6lWWOtkXkjTyCmBEOog0HSEKjIV4oAffw=="], + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260317.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-3QrNnPF1xlaNwkHpasvRvAMidOvQs2NhXQmALJrEfpIJ/IDL2la8g499yXp3eqhG3hVMCB07XVY149GTs42Xtw=="], - "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260312.1", "", { "os": "win32", "cpu": "x64" }, "sha512-5dBrlSK+nMsZy5bYQpj8t9iiQNvCRlkm9GGvswJa9vVU/1BNO4BhJMlqOLWT24EmFyApZ+kaBiPJMV8847NDTg=="], + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260317.1", "", { "os": "win32", "cpu": "x64" }, "sha512-MfZTz+7LfuIpMGTa3RLXHX8Z/pnycZLItn94WRdHr8LPVet+C5/1Nzei399w/jr3+kzT4pDKk26JF/tlI5elpQ=="], - "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260313.1", "", {}, "sha512-jMEeX3RKfOSVqqXRKr/ulgglcTloeMzSH3FdzIfqJHtvc12/ELKd5Ldsg8ZHahKX/4eRxYdw3kbzb8jLXbq/jQ=="], + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260317.1", "", {}, "sha512-+G4eVwyCpm8Au1ex8vQBCuA9wnwqetz4tPNRoB/53qvktERWBRMQnrtvC1k584yRE3emMThtuY0gWshvSJ++PQ=="], "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], @@ -182,7 +183,7 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], - "@kilianpaquier/semantic-release-backmerge": ["@kilianpaquier/semantic-release-backmerge@1.7.1", "", { "dependencies": { "@octokit/core": "7.0.6", "@semantic-release/error": "4.0.0", "aggregate-error": "5.0.0", "debug": "4.4.3", "execa": "9.6.1", "git-up": "8.1.1", "git-url-parse": "16.1.0", "lodash": "4.17.23", "node-fetch": "3.3.2", "semantic-release": "25.0.2", "semver": "7.7.3", "url-join": "5.0.0" } }, "sha512-R2ARmmzpcfkBm2SMMqdOkvb7a3uRtrHyXPUzZYHEBVjX5sQrv5d57sWs9NhFef2aTbL8bXqFredqSA+MqD7pxg=="], + "@kilianpaquier/semantic-release-backmerge": ["@kilianpaquier/semantic-release-backmerge@1.7.3", "", { "dependencies": { "@octokit/core": "7.0.6", "@semantic-release/error": "4.0.0", "aggregate-error": "5.0.0", "debug": "4.4.3", "execa": "9.6.1", "git-up": "8.1.1", "git-url-parse": "16.1.0", "lodash": "4.17.23", "node-fetch": "3.3.2", "semantic-release": "25.0.3", "semver": "7.7.4", "url-join": "5.0.0" } }, "sha512-H18O9omYVo925KF2sanfi5TnQ5Fh1jtsPIX7s+lC9CfWlWXJ4nywvCOY6ax+hYFZR/AFTG9zgHwD6AZL4HambA=="], "@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], @@ -330,7 +331,7 @@ "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], - "drizzle-kit": ["drizzle-kit@0.31.9", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg=="], + "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="], @@ -352,8 +353,6 @@ "esbuild": ["esbuild@0.25.12", "", { "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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -512,7 +511,7 @@ "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "miniflare": ["miniflare@4.20260312.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260312.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-pieP2rfXynPT6VRINYaiHe/tfMJ4c5OIhqRlIdLF6iZ9g5xgpEmvimvIgMpgAdDJuFlrLcwDUi8MfAo2R6dt/w=="], + "miniflare": ["miniflare@4.20260317.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.24.4", "workerd": "1.20260317.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-A3csI1HXEIfqe3oscgpoRMHdYlkReQKPH/g5JE53vFSjoM6YIAOGAzyDNeYffwd9oQkPWDj9xER8+vpxei8klA=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -596,6 +595,8 @@ "pkg-conf": ["pkg-conf@2.1.0", "", { "dependencies": { "find-up": "^2.0.0", "load-json-file": "^4.0.0" } }, "sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g=="], + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], @@ -624,9 +625,7 @@ "semantic-release": ["semantic-release@25.0.3", "", { "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", "@semantic-release/github": "^12.0.0", "@semantic-release/npm": "^13.1.1", "@semantic-release/release-notes-generator": "^14.1.0", "aggregate-error": "^5.0.0", "cosmiconfig": "^9.0.0", "debug": "^4.0.0", "env-ci": "^11.0.0", "execa": "^9.0.0", "figures": "^6.0.0", "find-versions": "^6.0.0", "get-stream": "^6.0.0", "git-log-parser": "^1.2.0", "hook-std": "^4.0.0", "hosted-git-info": "^9.0.0", "import-from-esm": "^2.0.0", "lodash-es": "^4.17.21", "marked": "^15.0.0", "marked-terminal": "^7.3.0", "micromatch": "^4.0.2", "p-each-series": "^3.0.0", "p-reduce": "^3.0.0", "read-package-up": "^12.0.0", "resolve-from": "^5.0.0", "semver": "^7.3.2", "signale": "^1.2.1", "yargs": "^18.0.0" }, "bin": "bin/semantic-release.js" }, "sha512-WRgl5GcypwramYX4HV+eQGzUbD7UUbljVmS+5G1uMwX/wLgYuJAxGeerXJDMO2xshng4+FXqCgyB5QfClV6WjA=="], - "semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "semver-diff": ["semver-diff@5.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-0HbGtOm+S7T6NGQ/pxJSJipJvc4DK3FcRVMRkhsIwJDJ4Jcz5DQC1cPPzB5GhzyHjwttW878HaWQq46CkL3cqg=="], + "semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "semver-regex": ["semver-regex@4.0.5", "", {}, "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw=="], @@ -700,6 +699,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], "type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="], @@ -738,9 +739,9 @@ "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], - "workerd": ["workerd@1.20260312.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260312.1", "@cloudflare/workerd-darwin-arm64": "1.20260312.1", "@cloudflare/workerd-linux-64": "1.20260312.1", "@cloudflare/workerd-linux-arm64": "1.20260312.1", "@cloudflare/workerd-windows-64": "1.20260312.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-nNpPkw9jaqo79B+iBCOiksx+N62xC+ETIfyzofUEdY3cSOHJg6oNnVSHm7vHevzVblfV76c8Gr0cXHEapYMBEg=="], + "workerd": ["workerd@1.20260317.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260317.1", "@cloudflare/workerd-darwin-arm64": "1.20260317.1", "@cloudflare/workerd-linux-64": "1.20260317.1", "@cloudflare/workerd-linux-arm64": "1.20260317.1", "@cloudflare/workerd-windows-64": "1.20260317.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-ZuEq1OdrJBS+NV+L5HMYPCzVn49a2O60slQiiLpG44jqtlOo+S167fWC76kEXteXLLLydeuRrluRel7WdOUa4g=="], - "wrangler": ["wrangler@4.73.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.15.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260312.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260312.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260312.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-VJXsqKDFCp6OtFEHXITSOR5kh95JOknwPY8m7RyQuWJQguSybJy43m4vhoCSt42prutTef7eeuw7L4V4xiynGw=="], + "wrangler": ["wrangler@4.76.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.16.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260317.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260317.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260317.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-Wan+CU5a0tu4HIxGOrzjNbkmxCT27HUmzrMj6kc7aoAnjSLv50Ggcn2Ant7wNQrD6xW3g31phKupZJgTZ8wZfQ=="], "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], @@ -768,8 +769,6 @@ "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": "bin/esbuild" }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], - "@kilianpaquier/semantic-release-backmerge/semantic-release": ["semantic-release@25.0.2", "", { "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", "@semantic-release/github": "^12.0.0", "@semantic-release/npm": "^13.1.1", "@semantic-release/release-notes-generator": "^14.1.0", "aggregate-error": "^5.0.0", "cosmiconfig": "^9.0.0", "debug": "^4.0.0", "env-ci": "^11.0.0", "execa": "^9.0.0", "figures": "^6.0.0", "find-versions": "^6.0.0", "get-stream": "^6.0.0", "git-log-parser": "^1.2.0", "hook-std": "^4.0.0", "hosted-git-info": "^9.0.0", "import-from-esm": "^2.0.0", "lodash-es": "^4.17.21", "marked": "^15.0.0", "marked-terminal": "^7.3.0", "micromatch": "^4.0.2", "p-each-series": "^3.0.0", "p-reduce": "^3.0.0", "read-package-up": "^12.0.0", "resolve-from": "^5.0.0", "semver": "^7.3.2", "semver-diff": "^5.0.0", "signale": "^1.2.1", "yargs": "^18.0.0" }, "bin": "bin/semantic-release.js" }, "sha512-6qGjWccl5yoyugHt3jTgztJ9Y0JVzyH8/Voc/D8PlLat9pwxQYXz7W1Dpnq5h0/G5GCYGUaDSlYcyk3AMh5A6g=="], - "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], "@poppinss/dumper/@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], @@ -786,8 +785,6 @@ "@semantic-release/git/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], - "@semantic-release/npm/semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "@semantic-release/release-notes-generator/get-stream": ["get-stream@7.0.1", "", {}, "sha512-3M8C1EOFN6r8AMUhwUAACIoXZJEOufDU5+0gFFN5uNs6XYOralD2Pqkl7m046va6x77FwposWXbAhPPIOus7mQ=="], "@semantic-release/release-notes-generator/read-package-up": ["read-package-up@11.0.0", "", { "dependencies": { "find-up-simple": "^1.0.0", "read-pkg": "^9.0.0", "type-fest": "^4.6.0" } }, "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ=="], @@ -798,8 +795,6 @@ "cli-table3/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "conventional-changelog-writer/semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "cosmiconfig/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], "crypto-random-string/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], @@ -816,9 +811,7 @@ "make-asynchronous/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "miniflare/undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="], - - "normalize-package-data/semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "miniflare/undici": ["undici@7.24.4", "", {}, "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], @@ -830,12 +823,6 @@ "semantic-release/p-reduce": ["p-reduce@3.0.0", "", {}, "sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q=="], - "semantic-release/semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - - "semver-diff/semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - - "sharp/semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "signale/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], "signale/figures": ["figures@2.0.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA=="], @@ -846,6 +833,8 @@ "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "tsx/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + "wrangler/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], @@ -894,8 +883,6 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@kilianpaquier/semantic-release-backmerge/semantic-release/p-reduce": ["p-reduce@3.0.0", "", {}, "sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q=="], - "@semantic-release/changelog/aggregate-error/clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], "@semantic-release/changelog/aggregate-error/indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], @@ -948,6 +935,58 @@ "signale/figures/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], @@ -1024,8 +1063,6 @@ "@semantic-release/release-notes-generator/read-package-up/read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], - "@semantic-release/release-notes-generator/read-package-up/read-pkg/normalize-package-data/semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "cli-highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "cli-highlight/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], diff --git a/drizzle.config.ts b/drizzle.config.ts index 31edb0e9..0d9f68b3 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,7 +1,7 @@ -import { defineConfig } from "drizzle-kit"; +import { defineConfig } from 'drizzle-kit'; export default defineConfig({ - out: "./drizzle/migrations", - schema: "./src/db/schema.ts", - dialect: "sqlite", + out: './drizzle/migrations', + schema: './src/db/schema.ts', + dialect: 'sqlite' }); diff --git a/package.json b/package.json index 442308a9..a5e7f76a 100644 --- a/package.json +++ b/package.json @@ -1,39 +1,41 @@ { - "name": "revanced-api", - "version": "1.8.0", - "description": "API server for ReVanced.", - "type": "module", - "scripts": { - "dev": "wrangler dev", - "deploy": "wrangler deploy", - "db:create": "wrangler d1 create revanced-api", - "db:migration:generate": "drizzle-kit generate", - "db:migration:apply": "wrangler d1 migrations apply revanced-api" - }, - "keywords": [ - "revanced", - "api", - "hono", - "cloudflare-workers" - ], - "license": "AGPL-3.0", - "dependencies": { - "@hono/swagger-ui": "^0.6.1", - "@hono/zod-openapi": "^1.2.2", - "drizzle-orm": "^0.45.1", - "hono": "^4.12.8", - "zod": "^4.3.6" - }, - "devDependencies": { - "@cloudflare/workers-types": "^4.20260313.1", - "@kilianpaquier/semantic-release-backmerge": "^1.7.1", - "@semantic-release/changelog": "^6.0.3", - "@semantic-release/git": "^10.0.1", - "@semantic-release/npm": "^13.1.5", - "@types/node": "^25.5.0", - "drizzle-kit": "^0.31.9", - "semantic-release": "^25.0.3", - "typescript": "^5.9.3", - "wrangler": "^4.73.0" - } + "name": "revanced-api", + "version": "1.8.0", + "description": "API server for ReVanced.", + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "format": "prettier --write --ignore-unknown .", + "db:create": "wrangler d1 create revanced-api", + "db:migration:generate": "drizzle-kit generate", + "db:migration:apply": "wrangler d1 migrations apply revanced-api" + }, + "keywords": [ + "revanced", + "api", + "hono", + "cloudflare-workers" + ], + "license": "AGPL-3.0", + "dependencies": { + "@hono/swagger-ui": "^0.6.1", + "@hono/zod-openapi": "^1.2.2", + "drizzle-orm": "^0.45.1", + "hono": "^4.12.8", + "zod": "^4.3.6" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20260317.1", + "@kilianpaquier/semantic-release-backmerge": "^1.7.3", + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/git": "^10.0.1", + "@semantic-release/npm": "^13.1.5", + "@types/node": "^25.5.0", + "drizzle-kit": "^0.31.10", + "prettier": "3.8.1", + "semantic-release": "^25.0.3", + "typescript": "^5.9.3", + "wrangler": "^4.76.0" + } } diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 6e05baa2..73a89e4d 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -1,23 +1,31 @@ -import type { MiddlewareHandler } from "hono"; -import type { Env } from "../types"; +import type { MiddlewareHandler } from 'hono'; +import type { Env } from '../types'; // Checks the Authorization header against the API_TOKEN env var. -export const authMiddleware: MiddlewareHandler<{ Bindings: Env }> = async (c, next) => { - const authHeader = c.req.header("Authorization"); +export const authMiddleware: MiddlewareHandler<{ Bindings: Env }> = async ( + c, + next +) => { + const authHeader = c.req.header('Authorization'); - if (!authHeader) { - return c.json({ error: "Missing Authorization header" }, 401); - } + if (!authHeader) { + return c.json({ error: 'Missing Authorization header' }, 401); + } - const [scheme, token] = authHeader.split(" ", 2); + const [scheme, token] = authHeader.split(' ', 2); - if (scheme !== "Bearer" || !token) { - return c.json({ error: "Invalid Authorization header format. Expected: Bearer " }, 401); - } + if (scheme !== 'Bearer' || !token) { + return c.json( + { + error: 'Invalid Authorization header format. Expected: Bearer ' + }, + 401 + ); + } - if (token !== c.env.API_TOKEN) { - return c.json({ error: "Invalid token" }, 403); - } + if (token !== c.env.API_TOKEN) { + return c.json({ error: 'Invalid token' }, 403); + } - await next(); + await next(); }; diff --git a/src/backend/github.ts b/src/backend/github.ts index bb7ca1d0..747d0cf4 100644 --- a/src/backend/github.ts +++ b/src/backend/github.ts @@ -1,170 +1,187 @@ import type { - Backend, - BackendRelease, - BackendAsset, - BackendContributor, - BackendMember, -} from "./types"; + Backend, + BackendRelease, + BackendAsset, + BackendContributor, + BackendMember +} from './types'; interface GitHubAsset { - name: string; - browser_download_url: string; + name: string; + browser_download_url: string; } interface GitHubRelease { - tag_name: string; - body: string; - created_at: string; - prerelease: boolean; - assets: GitHubAsset[]; + tag_name: string; + body: string; + created_at: string; + prerelease: boolean; + assets: GitHubAsset[]; } interface GitHubContributor { - login: string; - avatar_url: string; - html_url: string; - contributions: number; + login: string; + avatar_url: string; + html_url: string; + contributions: number; } interface GitHubMember { - login: string; - avatar_url: string; - html_url: string; + login: string; + avatar_url: string; + html_url: string; } interface GitHubUser { - login: string; - avatar_url: string; - html_url: string; - bio: string | null; + login: string; + avatar_url: string; + html_url: string; + bio: string | null; } interface GitHubGpgKey { - key_id: string; + key_id: string; } function formatDatetime(isoString: string): string { - return isoString - .replace(/\.\d{3}Z$/, "") - .replace(/Z$/, "") - .replace(/[+-]\d{2}:\d{2}$/, ""); + return isoString + .replace(/\.\d{3}Z$/, '') + .replace(/Z$/, '') + .replace(/[+-]\d{2}:\d{2}$/, ''); } export class GitHubBackend implements Backend { - private readonly baseUrl = "https://api.github.com"; - private readonly headers: HeadersInit; - - constructor(token?: string) { - const headers: Record = { - Accept: "application/vnd.github+json", - "User-Agent": "revanced-api", - }; - if (token) { - headers["Authorization"] = `Bearer ${token}`; + private readonly baseUrl = 'https://api.github.com'; + private readonly headers: HeadersInit; + + constructor(token?: string) { + const headers: Record = { + Accept: 'application/vnd.github+json', + 'User-Agent': 'revanced-api' + }; + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + this.headers = headers; } - this.headers = headers; - } - private async fetchJson(url: string): Promise { - const response = await fetch(url, { headers: this.headers }); - if (!response.ok) { - throw new Error(`GitHub API error: ${response.status} ${response.statusText} — ${url}`); - } - return response.json() as Promise; - } - - async release(owner: string, repo: string, prerelease: boolean): Promise { - let release: GitHubRelease; - - if (prerelease) { - const releases = await this.fetchJson( - `${this.baseUrl}/repos/${owner}/${repo}/releases?per_page=1`, - ); - if (releases.length === 0) { - throw new Error(`No releases found for ${owner}/${repo}`); - } - release = releases[0]; - } else { - release = await this.fetchJson( - `${this.baseUrl}/repos/${owner}/${repo}/releases/latest`, - ); + private async fetchJson(url: string): Promise { + const response = await fetch(url, { headers: this.headers }); + if (!response.ok) { + throw new Error( + `GitHub API error: ${response.status} ${response.statusText} — ${url}` + ); + } + return response.json() as Promise; } - return { - tag: release.tag_name, - releaseNote: release.body ?? "", - createdAt: formatDatetime(release.created_at), - prerelease: release.prerelease, - assets: release.assets.map( - (asset): BackendAsset => ({ - name: asset.name, - downloadUrl: asset.browser_download_url, - }), - ), - }; - } - - async releases(owner: string, repo: string, count: number): Promise { - const releases = await this.fetchJson( - `${this.baseUrl}/repos/${owner}/${repo}/releases?per_page=${count}`, - ); - - return releases.map((release) => ({ - tag: release.tag_name, - releaseNote: release.body ?? "", - createdAt: formatDatetime(release.created_at), - prerelease: release.prerelease, - assets: release.assets.map( - (asset): BackendAsset => ({ - name: asset.name, - downloadUrl: asset.browser_download_url, - }), - ), - })); - } - - async contributors(owner: string, repo: string): Promise { - const contributors = await this.fetchJson( - `${this.baseUrl}/repos/${owner}/${repo}/contributors?per_page=100`, - ); - - return contributors.map((contributor) => ({ - name: contributor.login, - avatarUrl: contributor.avatar_url, - url: contributor.html_url, - contributions: contributor.contributions, - })); - } - - async members(organization: string): Promise { - const publicMembers = await this.fetchJson( - `${this.baseUrl}/orgs/${organization}/public_members`, - ); - - const members = await Promise.all( - publicMembers.map(async (member) => { - const [user, gpgKeys] = await Promise.all([ - this.fetchJson(`${this.baseUrl}/users/${member.login}`), - this.fetchJson(`${this.baseUrl}/users/${member.login}/gpg_keys`), - ]); + async release( + owner: string, + repo: string, + prerelease: boolean + ): Promise { + let release: GitHubRelease; + + if (prerelease) { + const releases = await this.fetchJson( + `${this.baseUrl}/repos/${owner}/${repo}/releases?per_page=1` + ); + if (releases.length === 0) { + throw new Error(`No releases found for ${owner}/${repo}`); + } + release = releases[0]; + } else { + release = await this.fetchJson( + `${this.baseUrl}/repos/${owner}/${repo}/releases/latest` + ); + } return { - name: user.login, - avatarUrl: user.avatar_url, - url: user.html_url, - bio: user.bio, - gpgKeys: { - ids: gpgKeys.map((key) => key.key_id), - url: `https://github.com/${user.login}.gpg`, - }, - } satisfies BackendMember; - }), - ); - - return members; - } - - repositoryUrl(owner: string, repo: string): string { - return `https://github.com/${owner}/${repo}`; - } + tag: release.tag_name, + releaseNote: release.body ?? '', + createdAt: formatDatetime(release.created_at), + prerelease: release.prerelease, + assets: release.assets.map( + (asset): BackendAsset => ({ + name: asset.name, + downloadUrl: asset.browser_download_url + }) + ) + }; + } + + async releases( + owner: string, + repo: string, + count: number + ): Promise { + const releases = await this.fetchJson( + `${this.baseUrl}/repos/${owner}/${repo}/releases?per_page=${count}` + ); + + return releases.map((release) => ({ + tag: release.tag_name, + releaseNote: release.body ?? '', + createdAt: formatDatetime(release.created_at), + prerelease: release.prerelease, + assets: release.assets.map( + (asset): BackendAsset => ({ + name: asset.name, + downloadUrl: asset.browser_download_url + }) + ) + })); + } + + async contributors( + owner: string, + repo: string + ): Promise { + const contributors = await this.fetchJson( + `${this.baseUrl}/repos/${owner}/${repo}/contributors?per_page=100` + ); + + return contributors.map((contributor) => ({ + name: contributor.login, + avatarUrl: contributor.avatar_url, + url: contributor.html_url, + contributions: contributor.contributions + })); + } + + async members(organization: string): Promise { + const publicMembers = await this.fetchJson( + `${this.baseUrl}/orgs/${organization}/public_members` + ); + + const members = await Promise.all( + publicMembers.map(async (member) => { + const [user, gpgKeys] = await Promise.all([ + this.fetchJson( + `${this.baseUrl}/users/${member.login}` + ), + this.fetchJson( + `${this.baseUrl}/users/${member.login}/gpg_keys` + ) + ]); + + return { + name: user.login, + avatarUrl: user.avatar_url, + url: user.html_url, + bio: user.bio, + gpgKeys: { + ids: gpgKeys.map((key) => key.key_id), + url: `https://github.com/${user.login}.gpg` + } + } satisfies BackendMember; + }) + ); + + return members; + } + + repositoryUrl(owner: string, repo: string): string { + return `https://github.com/${owner}/${repo}`; + } } diff --git a/src/backend/types.ts b/src/backend/types.ts index 11d426eb..59487e5e 100644 --- a/src/backend/types.ts +++ b/src/backend/types.ts @@ -2,40 +2,48 @@ // Implement this to swap GitHub for GitLab, Gitea, or any other provider. export interface BackendRelease { - tag: string; - releaseNote: string; - createdAt: string; // ISO 8601 datetime without timezone suffix. - prerelease: boolean; - assets: BackendAsset[]; + tag: string; + releaseNote: string; + createdAt: string; // ISO 8601 datetime without timezone suffix. + prerelease: boolean; + assets: BackendAsset[]; } export interface BackendAsset { - name: string; - downloadUrl: string; + name: string; + downloadUrl: string; } export interface BackendContributor { - name: string; - avatarUrl: string; - url: string; - contributions: number; + name: string; + avatarUrl: string; + url: string; + contributions: number; } export interface BackendMember { - name: string; - avatarUrl: string; - url: string; - bio: string | null; - gpgKeys: { - ids: string[]; + name: string; + avatarUrl: string; url: string; - }; + bio: string | null; + gpgKeys: { + ids: string[]; + url: string; + }; } export interface Backend { - release(owner: string, repo: string, prerelease: boolean): Promise; - releases(owner: string, repo: string, count: number): Promise; - contributors(owner: string, repo: string): Promise; - members(organization: string): Promise; - repositoryUrl(owner: string, repo: string): string; + release( + owner: string, + repo: string, + prerelease: boolean + ): Promise; + releases( + owner: string, + repo: string, + count: number + ): Promise; + contributors(owner: string, repo: string): Promise; + members(organization: string): Promise; + repositoryUrl(owner: string, repo: string): string; } diff --git a/src/cache.ts b/src/cache.ts index 171aa4cc..40eb880c 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -1,15 +1,15 @@ -import type { MiddlewareHandler } from "hono"; +import type { MiddlewareHandler } from 'hono'; const SECONDS_PER_DAY = 86400; /** Cache duration presets matching the original Kotlin API. */ export const CacheDuration = { - /** 5 minutes — default for most API routes. */ - short: 5 * 60, - /** 1 day — for contributors, team, about. */ - day: SECONDS_PER_DAY, - /** 356 days — for public keys (essentially immutable). */ - immutable: 356 * SECONDS_PER_DAY, + /** 5 minutes — default for most API routes. */ + short: 5 * 60, + /** 1 day — for contributors, team, about. */ + day: SECONDS_PER_DAY, + /** 356 days — for public keys (essentially immutable). */ + immutable: 356 * SECONDS_PER_DAY } as const; /** @@ -17,9 +17,9 @@ export const CacheDuration = { * Cloudflare's CDN will respect `max-age` for edge caching. */ export function cacheControl(maxAgeSeconds: number): MiddlewareHandler { - const value = `public, max-age=${maxAgeSeconds}`; - return async (c, next) => { - c.header("Cache-Control", value); - await next(); - }; + const value = `public, max-age=${maxAgeSeconds}`; + return async (c, next) => { + c.header('Cache-Control', value); + await next(); + }; } diff --git a/src/config.ts b/src/config.ts index d4e41c9d..66af8ce8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,51 +1,53 @@ -import type { Env } from "./types"; -import { GitHubBackend } from "./backend/github"; +import type { Env } from './types'; +import { GitHubBackend } from './backend/github'; export interface Config { - organization: string; - patches: { - repo: string; - assetRegex: RegExp; - signatureAssetRegex: RegExp; - publicKeyFile: string; - }; - manager: { - repo: string; - assetRegex: RegExp; - downloadersRepo: string; - downloadersAssetRegex: RegExp; - }; - contributorRepos: { repo: string; name: string }[]; - apiVersion: string; + organization: string; + patches: { + repo: string; + assetRegex: RegExp; + signatureAssetRegex: RegExp; + publicKeyFile: string; + }; + manager: { + repo: string; + assetRegex: RegExp; + downloadersRepo: string; + downloadersAssetRegex: RegExp; + }; + contributorRepos: { repo: string; name: string }[]; + apiVersion: string; } let _config: Config | undefined; export function getConfig(env: Env): Config { - return (_config ??= { - organization: env.ORGANIZATION, - patches: { - repo: env.PATCHES_REPO, - assetRegex: new RegExp(env.PATCHES_ASSET_REGEX), - signatureAssetRegex: new RegExp(env.PATCHES_SIGNATURE_ASSET_REGEX), - publicKeyFile: env.PATCHES_PUBLIC_KEY_FILE, - }, - manager: { - repo: env.MANAGER_REPO, - assetRegex: new RegExp(env.MANAGER_ASSET_REGEX), - downloadersRepo: env.MANAGER_DOWNLOADERS_REPO, - downloadersAssetRegex: new RegExp(env.MANAGER_DOWNLOADERS_ASSET_REGEX), - }, - contributorRepos: env.CONTRIBUTORS_REPOS.split(",").map((entry) => { - const [repo, ...nameParts] = entry.trim().split(":"); - return { repo: repo.trim(), name: nameParts.join(":").trim() }; - }), - apiVersion: env.API_VERSION, - }); + return (_config ??= { + organization: env.ORGANIZATION, + patches: { + repo: env.PATCHES_REPO, + assetRegex: new RegExp(env.PATCHES_ASSET_REGEX), + signatureAssetRegex: new RegExp(env.PATCHES_SIGNATURE_ASSET_REGEX), + publicKeyFile: env.PATCHES_PUBLIC_KEY_FILE + }, + manager: { + repo: env.MANAGER_REPO, + assetRegex: new RegExp(env.MANAGER_ASSET_REGEX), + downloadersRepo: env.MANAGER_DOWNLOADERS_REPO, + downloadersAssetRegex: new RegExp( + env.MANAGER_DOWNLOADERS_ASSET_REGEX + ) + }, + contributorRepos: env.CONTRIBUTORS_REPOS.split(',').map((entry) => { + const [repo, ...nameParts] = entry.trim().split(':'); + return { repo: repo.trim(), name: nameParts.join(':').trim() }; + }), + apiVersion: env.API_VERSION + }); } let _backend: GitHubBackend | undefined; export function getBackend(env: Env): GitHubBackend { - return (_backend ??= new GitHubBackend(env.GITHUB_TOKEN)); + return (_backend ??= new GitHubBackend(env.GITHUB_TOKEN)); } diff --git a/src/db/client.ts b/src/db/client.ts index bd265791..e6e810c2 100644 --- a/src/db/client.ts +++ b/src/db/client.ts @@ -1,9 +1,9 @@ -import { drizzle } from "drizzle-orm/d1"; -import * as schema from "./schema"; -import type { Database } from "../types"; +import { drizzle } from 'drizzle-orm/d1'; +import * as schema from './schema'; +import type { Database } from '../types'; let _database: Database | undefined; export function getDatabase(d1: D1Database): Database { - return (_database ??= drizzle(d1, { schema })); + return (_database ??= drizzle(d1, { schema })); } diff --git a/src/db/schema.ts b/src/db/schema.ts index ddeffa5c..ce87de57 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,40 +1,40 @@ import { - index, - integer, - primaryKey, - sqliteTable, - text, -} from "drizzle-orm/sqlite-core"; + index, + integer, + primaryKey, + sqliteTable, + text +} from 'drizzle-orm/sqlite-core'; -export const announcements = sqliteTable("announcements", { - id: integer("id").primaryKey({ autoIncrement: true }), - author: text("author"), - title: text("title").notNull(), - content: text("content"), - createdAt: text("created_at") - .notNull() - .$defaultFn(() => new Date().toISOString()), - archivedAt: text("archived_at"), - level: integer("level").notNull().default(0), +export const announcements = sqliteTable('announcements', { + id: integer('id').primaryKey({ autoIncrement: true }), + author: text('author'), + title: text('title').notNull(), + content: text('content'), + createdAt: text('created_at') + .notNull() + .$defaultFn(() => new Date().toISOString()), + archivedAt: text('archived_at'), + level: integer('level').notNull().default(0) }); -export const tags = sqliteTable("tags", { - id: integer("id").primaryKey({ autoIncrement: true }), - name: text("name").notNull().unique(), +export const tags = sqliteTable('tags', { + id: integer('id').primaryKey({ autoIncrement: true }), + name: text('name').notNull().unique() }); export const announcementTags = sqliteTable( - "announcement_tags", - { - announcementId: integer("announcement_id") - .notNull() - .references(() => announcements.id, { onDelete: "cascade" }), - tagId: integer("tag_id") - .notNull() - .references(() => tags.id, { onDelete: "cascade" }), - }, - (table) => ({ - pk: primaryKey({ columns: [table.announcementId, table.tagId] }), - tagIdIdx: index("announcement_tags_tag_id_idx").on(table.tagId), - }), + 'announcement_tags', + { + announcementId: integer('announcement_id') + .notNull() + .references(() => announcements.id, { onDelete: 'cascade' }), + tagId: integer('tag_id') + .notNull() + .references(() => tags.id, { onDelete: 'cascade' }) + }, + (table) => ({ + pk: primaryKey({ columns: [table.announcementId, table.tagId] }), + tagIdIdx: index('announcement_tags_tag_id_idx').on(table.tagId) + }) ); diff --git a/src/index.ts b/src/index.ts index 4f1863f4..a4b6a902 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,91 +1,98 @@ -import { OpenAPIHono } from "@hono/zod-openapi"; -import { swaggerUI } from "@hono/swagger-ui"; -import type { Env } from "./types"; -import { cacheControl, CacheDuration } from "./cache"; -import { getConfig } from "./config"; -import packageJson from "../package.json"; -import patchesApp from "./routes/patches"; -import managerApp from "./routes/manager"; -import announcementsApp from "./routes/announcements"; -import contributorsApp from "./routes/contributors"; -import teamApp from "./routes/team"; -import aboutApp from "./routes/about"; +import { OpenAPIHono } from '@hono/zod-openapi'; +import { swaggerUI } from '@hono/swagger-ui'; +import type { Env } from './types'; +import { cacheControl, CacheDuration } from './cache'; +import { getConfig } from './config'; +import packageJson from '../package.json'; +import patchesApp from './routes/patches'; +import managerApp from './routes/manager'; +import announcementsApp from './routes/announcements'; +import contributorsApp from './routes/contributors'; +import teamApp from './routes/team'; +import aboutApp from './routes/about'; type AppBindings = { Bindings: Env }; let _app: OpenAPIHono | undefined; export default { - async fetch( - request: Request, - env: Env, - ctx: ExecutionContext, - ): Promise { - if (!_app) { - const { apiVersion } = getConfig(env); + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + if (!_app) { + const { apiVersion } = getConfig(env); - _app = new OpenAPIHono(); + _app = new OpenAPIHono(); - _app.onError((err, c) => { - console.error(err); - return c.json( - { - error: err.message || "Unknown error", - stack: err.stack, - }, - 500, - ); - }); + _app.onError((err, c) => { + console.error(err); + return c.json( + { + error: err.message || 'Unknown error', + stack: err.stack + }, + 500 + ); + }); - // Default 5-minute cache for all routes (overridden per-route where needed) - _app.use("*", cacheControl(CacheDuration.short)); + // Default 5-minute cache for all routes (overridden per-route where needed) + _app.use('*', cacheControl(CacheDuration.short)); - const versionedApp = new OpenAPIHono(); - versionedApp.route("/patches", patchesApp); - versionedApp.route("/manager", managerApp); - versionedApp.route("/announcements", announcementsApp); - versionedApp.route("/contributors", contributorsApp); - versionedApp.route("/team", teamApp); - versionedApp.route("/about", aboutApp); + const versionedApp = new OpenAPIHono(); + versionedApp.route('/patches', patchesApp); + versionedApp.route('/manager', managerApp); + versionedApp.route('/announcements', announcementsApp); + versionedApp.route('/contributors', contributorsApp); + versionedApp.route('/team', teamApp); + versionedApp.route('/about', aboutApp); - _app.route(`/v${apiVersion}`, versionedApp); - _app.get("/", swaggerUI({ url: `/v${apiVersion}/openapi` })); + _app.route(`/v${apiVersion}`, versionedApp); + _app.get('/', swaggerUI({ url: `/v${apiVersion}/openapi` })); - _app.doc(`/v${apiVersion}/openapi`, () => ({ - openapi: "3.1.0", - info: { - title: "ReVanced API", - version: packageJson.version, - description: "API server for ReVanced.", - contact: { - name: "ReVanced", - url: "https://revanced.app", - email: "contact@revanced.app", - }, - license: { - name: "AGPLv3", - url: "https://github.com/ReVanced/revanced-api/blob/main/LICENSE", - }, - }, - servers: [ - { url: "https://api.revanced.app", description: "Production" }, - { - url: "{customServer}", - description: "Custom server", - variables: { - customServer: { - default: "api.revanced.app", - description: "Custom server URL", - }, - }, - }, - ], - })); - _app.openAPIRegistry.registerComponent("securitySchemes", "Bearer", { - type: "http", - scheme: "bearer", - }); - } - return _app.fetch(request, env, ctx); - }, + _app.doc(`/v${apiVersion}/openapi`, () => ({ + openapi: '3.1.0', + info: { + title: 'ReVanced API', + version: packageJson.version, + description: 'API server for ReVanced.', + contact: { + name: 'ReVanced', + url: 'https://revanced.app', + email: 'contact@revanced.app' + }, + license: { + name: 'AGPLv3', + url: 'https://github.com/ReVanced/revanced-api/blob/main/LICENSE' + } + }, + servers: [ + { + url: 'https://api.revanced.app', + description: 'Production' + }, + { + url: '{customServer}', + description: 'Custom server', + variables: { + customServer: { + default: 'api.revanced.app', + description: 'Custom server URL' + } + } + } + ] + })); + _app.openAPIRegistry.registerComponent( + 'securitySchemes', + 'Bearer', + { + type: 'http', + scheme: 'bearer' + } + ); + } + return _app.fetch(request, env, ctx); + } }; diff --git a/src/routes/about.ts b/src/routes/about.ts index 28856797..4bcbcfad 100644 --- a/src/routes/about.ts +++ b/src/routes/about.ts @@ -1,87 +1,89 @@ -import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; -import type { Env } from "../types"; -import aboutData from "about.json"; -import { cacheControl, CacheDuration } from "../cache"; +import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; +import type { Env } from '../types'; +import aboutData from 'about.json'; +import { cacheControl, CacheDuration } from '../cache'; const app = new OpenAPIHono<{ Bindings: Env }>(); // 1-day cache for about info -app.use("*", cacheControl(CacheDuration.day)); +app.use('*', cacheControl(CacheDuration.day)); const AboutResponseSchema = z - .object({ - name: z.string(), - about: z.string(), - keys: z.string(), - branding: z - .object({ - logo: z.string().url(), - }) - .optional() - .nullable(), - status: z.string(), - contact: z - .object({ - email: z.string().email(), - }) - .optional() - .nullable(), - socials: z - .array( - z.object({ - name: z.string(), - url: z.string().url(), - preferred: z.boolean().optional(), - }), - ) - .optional() - .nullable(), - donations: z - .object({ - wallets: z - .array( - z.object({ - network: z.string(), - currency_code: z.string(), - address: z.string(), - preferred: z.boolean().optional(), - }), - ) - .optional() - .nullable(), - links: z - .array( - z.object({ - name: z.string(), - url: z.string().url(), - preferred: z.boolean().optional(), - }), - ) - .optional() - .nullable(), - }) - .optional() - .nullable(), - }) - .openapi("About"); + .object({ + name: z.string(), + about: z.string(), + keys: z.string(), + branding: z + .object({ + logo: z.string().url() + }) + .optional() + .nullable(), + status: z.string(), + contact: z + .object({ + email: z.string().email() + }) + .optional() + .nullable(), + socials: z + .array( + z.object({ + name: z.string(), + url: z.string().url(), + preferred: z.boolean().optional() + }) + ) + .optional() + .nullable(), + donations: z + .object({ + wallets: z + .array( + z.object({ + network: z.string(), + currency_code: z.string(), + address: z.string(), + preferred: z.boolean().optional() + }) + ) + .optional() + .nullable(), + links: z + .array( + z.object({ + name: z.string(), + url: z.string().url(), + preferred: z.boolean().optional() + }) + ) + .optional() + .nullable() + }) + .optional() + .nullable() + }) + .openapi('About'); app.openapi( - createRoute({ - method: "get", - path: "/", - tags: ["API"], - summary: "Get about", - description: "Get information about the API.", - responses: { - 200: { - content: { "application/json": { schema: AboutResponseSchema } }, - description: "Information about the API.", - }, - }, - }), - (c) => { - return c.json(aboutData as z.infer, 200); - }, + createRoute({ + method: 'get', + path: '/', + tags: ['API'], + summary: 'Get about', + description: 'Get information about the API.', + responses: { + 200: { + content: { + 'application/json': { schema: AboutResponseSchema } + }, + description: 'Information about the API.' + } + } + }), + (c) => { + return c.json(aboutData as z.infer, 200); + } ); export default app; diff --git a/src/routes/announcements.ts b/src/routes/announcements.ts index 75fe642a..5670acfd 100644 --- a/src/routes/announcements.ts +++ b/src/routes/announcements.ts @@ -1,219 +1,246 @@ -import { OpenAPIHono, createRoute } from "@hono/zod-openapi"; -import type { Env } from "../types"; -import { authMiddleware } from "../auth/auth"; -import { ErrorResponseSchema } from "../schemas/common"; +import { OpenAPIHono, createRoute } from '@hono/zod-openapi'; +import type { Env } from '../types'; +import { authMiddleware } from '../auth/auth'; +import { ErrorResponseSchema } from '../schemas/common'; import { - AnnouncementIdParamSchema, - AnnouncementResponseSchema, - AnnouncementsResponseSchema, - CreateAnnouncementBodySchema, - LatestAnnouncementIdsResponseSchema, - LatestAnnouncementsResponseSchema, - UpdateAnnouncementBodySchema, -} from "../schemas/announcements"; -import * as announcementsService from "../services/announcements"; + AnnouncementIdParamSchema, + AnnouncementResponseSchema, + AnnouncementsResponseSchema, + CreateAnnouncementBodySchema, + LatestAnnouncementIdsResponseSchema, + LatestAnnouncementsResponseSchema, + UpdateAnnouncementBodySchema +} from '../schemas/announcements'; +import * as announcementsService from '../services/announcements'; const app = new OpenAPIHono<{ Bindings: Env }>(); app.openapi( - createRoute({ - method: "get", - path: "/", - tags: ["Announcements"], - summary: "Get all announcements", - description: "Get all announcements ordered by newest first.", - responses: { - 200: { - content: { - "application/json": { schema: AnnouncementsResponseSchema }, - }, - description: "All announcements.", - }, - }, - }), - async (c) => { - return c.json(await announcementsService.listAnnouncements(c.env), 200); - }, + createRoute({ + method: 'get', + path: '/', + tags: ['Announcements'], + summary: 'Get all announcements', + description: 'Get all announcements ordered by newest first.', + responses: { + 200: { + content: { + 'application/json': { schema: AnnouncementsResponseSchema } + }, + description: 'All announcements.' + } + } + }), + async (c) => { + return c.json(await announcementsService.listAnnouncements(c.env), 200); + } ); app.openapi( - createRoute({ - method: "get", - path: "/latest", - tags: ["Announcements"], - summary: "Get the latest announcement for each tag", - description: - "Get the newest announcement for every available announcement tag.", - responses: { - 200: { - content: { - "application/json": { schema: LatestAnnouncementsResponseSchema }, - }, - description: "The newest announcement for each tag.", - }, - }, - }), - async (c) => { - return c.json( - await announcementsService.getLatestAnnouncementsByTag(c.env), - 200, - ); - }, + createRoute({ + method: 'get', + path: '/latest', + tags: ['Announcements'], + summary: 'Get the latest announcement for each tag', + description: + 'Get the newest announcement for every available announcement tag.', + responses: { + 200: { + content: { + 'application/json': { + schema: LatestAnnouncementsResponseSchema + } + }, + description: 'The newest announcement for each tag.' + } + } + }), + async (c) => { + return c.json( + await announcementsService.getLatestAnnouncementsByTag(c.env), + 200 + ); + } ); app.openapi( - createRoute({ - method: "get", - path: "/latest/id", - tags: ["Announcements"], - summary: "Get the latest announcement ID for each tag", - description: - "Get the ID of the newest announcement for every available announcement tag.", - responses: { - 200: { - content: { - "application/json": { schema: LatestAnnouncementIdsResponseSchema }, - }, - description: "The newest announcement ID for each tag.", - }, - }, - }), - async (c) => { - return c.json( - await announcementsService.getLatestAnnouncementIdsByTag(c.env), - 200, - ); - }, + createRoute({ + method: 'get', + path: '/latest/id', + tags: ['Announcements'], + summary: 'Get the latest announcement ID for each tag', + description: + 'Get the ID of the newest announcement for every available announcement tag.', + responses: { + 200: { + content: { + 'application/json': { + schema: LatestAnnouncementIdsResponseSchema + } + }, + description: 'The newest announcement ID for each tag.' + } + } + }), + async (c) => { + return c.json( + await announcementsService.getLatestAnnouncementIdsByTag(c.env), + 200 + ); + } ); app.openapi( - createRoute({ - method: "post", - path: "/", - tags: ["Announcements"], - summary: "Create an announcement", - description: - "Create a new announcement. Requires bearer token authentication.", - security: [{ Bearer: [] }], - middleware: [authMiddleware], - request: { - body: { - content: { - "application/json": { schema: CreateAnnouncementBodySchema }, - }, - }, - }, - responses: { - 201: { - content: { "application/json": { schema: AnnouncementResponseSchema } }, - description: "The created announcement.", - }, - 401: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "Unauthorized.", - }, - 403: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "Forbidden.", - }, - }, - }), - async (c) => { - const body = c.req.valid("json"); - return c.json( - await announcementsService.createAnnouncement(c.env, body), - 201, - ); - }, + createRoute({ + method: 'post', + path: '/', + tags: ['Announcements'], + summary: 'Create an announcement', + description: + 'Create a new announcement. Requires bearer token authentication.', + security: [{ Bearer: [] }], + middleware: [authMiddleware], + request: { + body: { + content: { + 'application/json': { schema: CreateAnnouncementBodySchema } + } + } + }, + responses: { + 201: { + content: { + 'application/json': { schema: AnnouncementResponseSchema } + }, + description: 'The created announcement.' + }, + 401: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'Unauthorized.' + }, + 403: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'Forbidden.' + } + } + }), + async (c) => { + const body = c.req.valid('json'); + return c.json( + await announcementsService.createAnnouncement(c.env, body), + 201 + ); + } ); app.openapi( - createRoute({ - method: "patch", - path: "/{id}", - tags: ["Announcements"], - summary: "Update an announcement", - description: - "Update an existing announcement. Requires bearer token authentication.", - security: [{ Bearer: [] }], - middleware: [authMiddleware], - request: { - params: AnnouncementIdParamSchema, - body: { - content: { - "application/json": { schema: UpdateAnnouncementBodySchema }, - }, - }, - }, - responses: { - 200: { - content: { "application/json": { schema: AnnouncementResponseSchema } }, - description: "The updated announcement.", - }, - 401: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "Unauthorized.", - }, - 403: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "Forbidden.", - }, - 404: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "Announcement not found.", - }, - }, - }), - async (c) => { - const { id } = c.req.valid("param"); - const body = c.req.valid("json"); - const result = await announcementsService.updateAnnouncement( - c.env, - id, - body, - ); - if (!result) { - return c.json({ error: "Announcement not found" }, 404); - } - return c.json(result, 200); - }, + createRoute({ + method: 'patch', + path: '/{id}', + tags: ['Announcements'], + summary: 'Update an announcement', + description: + 'Update an existing announcement. Requires bearer token authentication.', + security: [{ Bearer: [] }], + middleware: [authMiddleware], + request: { + params: AnnouncementIdParamSchema, + body: { + content: { + 'application/json': { schema: UpdateAnnouncementBodySchema } + } + } + }, + responses: { + 200: { + content: { + 'application/json': { schema: AnnouncementResponseSchema } + }, + description: 'The updated announcement.' + }, + 401: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'Unauthorized.' + }, + 403: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'Forbidden.' + }, + 404: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'Announcement not found.' + } + } + }), + async (c) => { + const { id } = c.req.valid('param'); + const body = c.req.valid('json'); + const result = await announcementsService.updateAnnouncement( + c.env, + id, + body + ); + if (!result) { + return c.json({ error: 'Announcement not found' }, 404); + } + return c.json(result, 200); + } ); app.openapi( - createRoute({ - method: "delete", - path: "/{id}", - tags: ["Announcements"], - summary: "Delete an announcement", - description: - "Delete an announcement. Requires bearer token authentication.", - security: [{ Bearer: [] }], - middleware: [authMiddleware], - request: { params: AnnouncementIdParamSchema }, - responses: { - 204: { description: "Announcement deleted." }, - 401: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "Unauthorized.", - }, - 403: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "Forbidden.", - }, - 404: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "Announcement not found.", - }, - }, - }), - async (c) => { - const { id } = c.req.valid("param"); - const deleted = await announcementsService.deleteAnnouncement(c.env, id); - if (!deleted) { - return c.json({ error: "Announcement not found" }, 404); - } - return c.body(null, 204); - }, + createRoute({ + method: 'delete', + path: '/{id}', + tags: ['Announcements'], + summary: 'Delete an announcement', + description: + 'Delete an announcement. Requires bearer token authentication.', + security: [{ Bearer: [] }], + middleware: [authMiddleware], + request: { params: AnnouncementIdParamSchema }, + responses: { + 204: { description: 'Announcement deleted.' }, + 401: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'Unauthorized.' + }, + 403: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'Forbidden.' + }, + 404: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'Announcement not found.' + } + } + }), + async (c) => { + const { id } = c.req.valid('param'); + const deleted = await announcementsService.deleteAnnouncement( + c.env, + id + ); + if (!deleted) { + return c.json({ error: 'Announcement not found' }, 404); + } + return c.body(null, 204); + } ); export default app; diff --git a/src/routes/contributors.ts b/src/routes/contributors.ts index 981dc00a..6631991b 100644 --- a/src/routes/contributors.ts +++ b/src/routes/contributors.ts @@ -1,36 +1,41 @@ -import { OpenAPIHono, createRoute } from "@hono/zod-openapi"; -import type { Env } from "../types"; -import { ErrorResponseSchema } from "../schemas/common"; -import { ContributorsResponseSchema } from "../schemas/contributors"; -import * as contributorsService from "../services/contributors"; -import { cacheControl, CacheDuration } from "../cache"; +import { OpenAPIHono, createRoute } from '@hono/zod-openapi'; +import type { Env } from '../types'; +import { ErrorResponseSchema } from '../schemas/common'; +import { ContributorsResponseSchema } from '../schemas/contributors'; +import * as contributorsService from '../services/contributors'; +import { cacheControl, CacheDuration } from '../cache'; const app = new OpenAPIHono<{ Bindings: Env }>(); // 1-day cache for contributors -app.use("*", cacheControl(CacheDuration.day)); +app.use('*', cacheControl(CacheDuration.day)); app.openapi( - createRoute({ - method: "get", - path: "/", - tags: ["API"], - summary: "Get contributors", - description: "Get the list of contributors for each configured repository.", - responses: { - 200: { - content: { "application/json": { schema: ContributorsResponseSchema } }, - description: "The list of contributors.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json(await contributorsService.getContributors(c.env), 200); - }, + createRoute({ + method: 'get', + path: '/', + tags: ['API'], + summary: 'Get contributors', + description: + 'Get the list of contributors for each configured repository.', + responses: { + 200: { + content: { + 'application/json': { schema: ContributorsResponseSchema } + }, + description: 'The list of contributors.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json(await contributorsService.getContributors(c.env), 200); + } ); export default app; diff --git a/src/routes/manager.ts b/src/routes/manager.ts index 003bf805..c989b974 100644 --- a/src/routes/manager.ts +++ b/src/routes/manager.ts @@ -1,249 +1,297 @@ -import { OpenAPIHono, createRoute } from "@hono/zod-openapi"; -import type { Env } from "../types"; -import { ErrorResponseSchema } from "../schemas/common"; +import { OpenAPIHono, createRoute } from '@hono/zod-openapi'; +import type { Env } from '../types'; +import { ErrorResponseSchema } from '../schemas/common'; import { - ReleaseResponseSchema, - VersionResponseSchema, - HistoryResponseSchema, -} from "../schemas/releases"; -import * as managerService from "../services/manager"; + ReleaseResponseSchema, + VersionResponseSchema, + HistoryResponseSchema +} from '../schemas/releases'; +import * as managerService from '../services/manager'; const app = new OpenAPIHono<{ Bindings: Env }>(); app.openapi( - createRoute({ - method: "get", - path: "/", - tags: ["Manager"], - summary: "Get current manager release", - description: "Get the current stable manager release.", - responses: { - 200: { - content: { "application/json": { schema: ReleaseResponseSchema } }, - description: "The latest manager release.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json(await managerService.getRelease(c.env, false), 200); - }, + createRoute({ + method: 'get', + path: '/', + tags: ['Manager'], + summary: 'Get current manager release', + description: 'Get the current stable manager release.', + responses: { + 200: { + content: { + 'application/json': { schema: ReleaseResponseSchema } + }, + description: 'The latest manager release.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json(await managerService.getRelease(c.env, false), 200); + } ); app.openapi( - createRoute({ - method: "get", - path: "/prerelease", - tags: ["Manager"], - summary: "Get current manager prerelease", - description: "Get the current manager prerelease.", - responses: { - 200: { - content: { "application/json": { schema: ReleaseResponseSchema } }, - description: "The latest manager prerelease.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json(await managerService.getRelease(c.env, true), 200); - }, + createRoute({ + method: 'get', + path: '/prerelease', + tags: ['Manager'], + summary: 'Get current manager prerelease', + description: 'Get the current manager prerelease.', + responses: { + 200: { + content: { + 'application/json': { schema: ReleaseResponseSchema } + }, + description: 'The latest manager prerelease.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json(await managerService.getRelease(c.env, true), 200); + } ); app.openapi( - createRoute({ - method: "get", - path: "/version", - tags: ["Manager"], - summary: "Get current manager release version", - description: "Get the current stable manager release version.", - responses: { - 200: { - content: { "application/json": { schema: VersionResponseSchema } }, - description: "The current manager release version.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json(await managerService.getVersion(c.env, false), 200); - }, + createRoute({ + method: 'get', + path: '/version', + tags: ['Manager'], + summary: 'Get current manager release version', + description: 'Get the current stable manager release version.', + responses: { + 200: { + content: { + 'application/json': { schema: VersionResponseSchema } + }, + description: 'The current manager release version.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json(await managerService.getVersion(c.env, false), 200); + } ); app.openapi( - createRoute({ - method: "get", - path: "/version/prerelease", - tags: ["Manager"], - summary: "Get current manager prerelease version", - description: "Get the current manager prerelease version.", - responses: { - 200: { - content: { "application/json": { schema: VersionResponseSchema } }, - description: "The current manager prerelease version.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json(await managerService.getVersion(c.env, true), 200); - }, + createRoute({ + method: 'get', + path: '/version/prerelease', + tags: ['Manager'], + summary: 'Get current manager prerelease version', + description: 'Get the current manager prerelease version.', + responses: { + 200: { + content: { + 'application/json': { schema: VersionResponseSchema } + }, + description: 'The current manager prerelease version.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json(await managerService.getVersion(c.env, true), 200); + } ); app.openapi( - createRoute({ - method: "get", - path: "/history", - tags: ["Manager"], - summary: "Get manager release history", - description: "Get the stable manager release history.", - responses: { - 200: { - content: { "application/json": { schema: HistoryResponseSchema } }, - description: "The manager release history.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json(await managerService.getHistory(c.env, false), 200); - }, + createRoute({ + method: 'get', + path: '/history', + tags: ['Manager'], + summary: 'Get manager release history', + description: 'Get the stable manager release history.', + responses: { + 200: { + content: { + 'application/json': { schema: HistoryResponseSchema } + }, + description: 'The manager release history.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json(await managerService.getHistory(c.env, false), 200); + } ); app.openapi( - createRoute({ - method: "get", - path: "/history/prerelease", - tags: ["Manager"], - summary: "Get manager prerelease history", - description: "Get the manager prerelease history.", - responses: { - 200: { - content: { "application/json": { schema: HistoryResponseSchema } }, - description: "The manager prerelease history.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json(await managerService.getHistory(c.env, true), 200); - }, + createRoute({ + method: 'get', + path: '/history/prerelease', + tags: ['Manager'], + summary: 'Get manager prerelease history', + description: 'Get the manager prerelease history.', + responses: { + 200: { + content: { + 'application/json': { schema: HistoryResponseSchema } + }, + description: 'The manager prerelease history.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json(await managerService.getHistory(c.env, true), 200); + } ); app.openapi( - createRoute({ - method: "get", - path: "/downloaders", - tags: ["Manager"], - summary: "Get current manager downloaders release", - description: "Get the current stable manager downloaders release.", - responses: { - 200: { - content: { "application/json": { schema: ReleaseResponseSchema } }, - description: "The latest manager downloaders release.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json( - await managerService.getDownloadersRelease(c.env, false), - 200, - ); - }, + createRoute({ + method: 'get', + path: '/downloaders', + tags: ['Manager'], + summary: 'Get current manager downloaders release', + description: 'Get the current stable manager downloaders release.', + responses: { + 200: { + content: { + 'application/json': { schema: ReleaseResponseSchema } + }, + description: 'The latest manager downloaders release.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json( + await managerService.getDownloadersRelease(c.env, false), + 200 + ); + } ); app.openapi( - createRoute({ - method: "get", - path: "/downloaders/prerelease", - tags: ["Manager"], - summary: "Get current manager downloaders prerelease", - description: "Get the current manager downloaders prerelease.", - responses: { - 200: { - content: { "application/json": { schema: ReleaseResponseSchema } }, - description: "The latest manager downloaders prerelease.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json(await managerService.getDownloadersRelease(c.env, true), 200); - }, + createRoute({ + method: 'get', + path: '/downloaders/prerelease', + tags: ['Manager'], + summary: 'Get current manager downloaders prerelease', + description: 'Get the current manager downloaders prerelease.', + responses: { + 200: { + content: { + 'application/json': { schema: ReleaseResponseSchema } + }, + description: 'The latest manager downloaders prerelease.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json( + await managerService.getDownloadersRelease(c.env, true), + 200 + ); + } ); app.openapi( - createRoute({ - method: "get", - path: "/downloaders/version", - tags: ["Manager"], - summary: "Get current manager downloaders release version", - description: "Get the current stable manager downloaders release version.", - responses: { - 200: { - content: { "application/json": { schema: VersionResponseSchema } }, - description: "The current manager downloaders release version.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json( - await managerService.getDownloadersVersion(c.env, false), - 200, - ); - }, + createRoute({ + method: 'get', + path: '/downloaders/version', + tags: ['Manager'], + summary: 'Get current manager downloaders release version', + description: + 'Get the current stable manager downloaders release version.', + responses: { + 200: { + content: { + 'application/json': { schema: VersionResponseSchema } + }, + description: 'The current manager downloaders release version.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json( + await managerService.getDownloadersVersion(c.env, false), + 200 + ); + } ); app.openapi( - createRoute({ - method: "get", - path: "/downloaders/version/prerelease", - tags: ["Manager"], - summary: "Get current manager downloaders prerelease version", - description: "Get the current manager downloaders prerelease version.", - responses: { - 200: { - content: { "application/json": { schema: VersionResponseSchema } }, - description: "The current manager downloaders prerelease version.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json(await managerService.getDownloadersVersion(c.env, true), 200); - }, + createRoute({ + method: 'get', + path: '/downloaders/version/prerelease', + tags: ['Manager'], + summary: 'Get current manager downloaders prerelease version', + description: 'Get the current manager downloaders prerelease version.', + responses: { + 200: { + content: { + 'application/json': { schema: VersionResponseSchema } + }, + description: + 'The current manager downloaders prerelease version.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json( + await managerService.getDownloadersVersion(c.env, true), + 200 + ); + } ); export default app; diff --git a/src/routes/patches.ts b/src/routes/patches.ts index 72e6a53c..8adc5c4b 100644 --- a/src/routes/patches.ts +++ b/src/routes/patches.ts @@ -1,173 +1,199 @@ -import { OpenAPIHono, createRoute } from "@hono/zod-openapi"; -import type { Env } from "../types"; -import { ErrorResponseSchema } from "../schemas/common"; +import { OpenAPIHono, createRoute } from '@hono/zod-openapi'; +import type { Env } from '../types'; +import { ErrorResponseSchema } from '../schemas/common'; import { - ReleaseResponseSchema, - VersionResponseSchema, - HistoryResponseSchema, - PublicKeyResponseSchema, -} from "../schemas/releases"; -import * as patchesService from "../services/patches"; + ReleaseResponseSchema, + VersionResponseSchema, + HistoryResponseSchema, + PublicKeyResponseSchema +} from '../schemas/releases'; +import * as patchesService from '../services/patches'; const app = new OpenAPIHono<{ Bindings: Env }>(); app.openapi( - createRoute({ - method: "get", - path: "/", - tags: ["Patches"], - summary: "Get current patches release", - description: "Get the current stable patches release.", - responses: { - 200: { - content: { "application/json": { schema: ReleaseResponseSchema } }, - description: "The current patches release.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json(await patchesService.getRelease(c.env, false), 200); - }, + createRoute({ + method: 'get', + path: '/', + tags: ['Patches'], + summary: 'Get current patches release', + description: 'Get the current stable patches release.', + responses: { + 200: { + content: { + 'application/json': { schema: ReleaseResponseSchema } + }, + description: 'The current patches release.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json(await patchesService.getRelease(c.env, false), 200); + } ); app.openapi( - createRoute({ - method: "get", - path: "/prerelease", - tags: ["Patches"], - summary: "Get current patches prerelease", - description: "Get the current patches prerelease.", - responses: { - 200: { - content: { "application/json": { schema: ReleaseResponseSchema } }, - description: "The current patches prerelease.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json(await patchesService.getRelease(c.env, true), 200); - }, + createRoute({ + method: 'get', + path: '/prerelease', + tags: ['Patches'], + summary: 'Get current patches prerelease', + description: 'Get the current patches prerelease.', + responses: { + 200: { + content: { + 'application/json': { schema: ReleaseResponseSchema } + }, + description: 'The current patches prerelease.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json(await patchesService.getRelease(c.env, true), 200); + } ); // --- Version --- app.openapi( - createRoute({ - method: "get", - path: "/version", - tags: ["Patches"], - summary: "Get current patches release version", - description: "Get the current stable patches release version.", - responses: { - 200: { - content: { "application/json": { schema: VersionResponseSchema } }, - description: "The current patches release version.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json(await patchesService.getVersion(c.env, false), 200); - }, + createRoute({ + method: 'get', + path: '/version', + tags: ['Patches'], + summary: 'Get current patches release version', + description: 'Get the current stable patches release version.', + responses: { + 200: { + content: { + 'application/json': { schema: VersionResponseSchema } + }, + description: 'The current patches release version.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json(await patchesService.getVersion(c.env, false), 200); + } ); app.openapi( - createRoute({ - method: "get", - path: "/version/prerelease", - tags: ["Patches"], - summary: "Get current patches prerelease version", - description: "Get the current patches prerelease version.", - responses: { - 200: { - content: { "application/json": { schema: VersionResponseSchema } }, - description: "The current patches prerelease version.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json(await patchesService.getVersion(c.env, true), 200); - }, + createRoute({ + method: 'get', + path: '/version/prerelease', + tags: ['Patches'], + summary: 'Get current patches prerelease version', + description: 'Get the current patches prerelease version.', + responses: { + 200: { + content: { + 'application/json': { schema: VersionResponseSchema } + }, + description: 'The current patches prerelease version.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json(await patchesService.getVersion(c.env, true), 200); + } ); app.openapi( - createRoute({ - method: "get", - path: "/history", - tags: ["Patches"], - summary: "Get patches release history", - description: "Get the stable patches release history.", - responses: { - 200: { - content: { "application/json": { schema: HistoryResponseSchema } }, - description: "The patches release history.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json(await patchesService.getHistory(c.env, false), 200); - }, + createRoute({ + method: 'get', + path: '/history', + tags: ['Patches'], + summary: 'Get patches release history', + description: 'Get the stable patches release history.', + responses: { + 200: { + content: { + 'application/json': { schema: HistoryResponseSchema } + }, + description: 'The patches release history.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json(await patchesService.getHistory(c.env, false), 200); + } ); app.openapi( - createRoute({ - method: "get", - path: "/history/prerelease", - tags: ["Patches"], - summary: "Get patches prerelease history", - description: "Get the patches prerelease history.", - responses: { - 200: { - content: { "application/json": { schema: HistoryResponseSchema } }, - description: "The patches prerelease history.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json(await patchesService.getHistory(c.env, true), 200); - }, + createRoute({ + method: 'get', + path: '/history/prerelease', + tags: ['Patches'], + summary: 'Get patches prerelease history', + description: 'Get the patches prerelease history.', + responses: { + 200: { + content: { + 'application/json': { schema: HistoryResponseSchema } + }, + description: 'The patches prerelease history.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json(await patchesService.getHistory(c.env, true), 200); + } ); app.openapi( - createRoute({ - method: "get", - path: "/keys", - tags: ["Patches"], - summary: "Get patches public keys", - description: "Get the public keys for verifying patches assets.", - responses: { - 200: { - content: { "application/json": { schema: PublicKeyResponseSchema } }, - description: "The public keys.", - }, - }, - }), - async (c) => { - return c.json(await patchesService.getPublicKey(c.env), 200); - }, + createRoute({ + method: 'get', + path: '/keys', + tags: ['Patches'], + summary: 'Get patches public keys', + description: 'Get the public keys for verifying patches assets.', + responses: { + 200: { + content: { + 'application/json': { schema: PublicKeyResponseSchema } + }, + description: 'The public keys.' + } + } + }), + async (c) => { + return c.json(await patchesService.getPublicKey(c.env), 200); + } ); export default app; diff --git a/src/routes/team.ts b/src/routes/team.ts index a2366381..f6dbaf97 100644 --- a/src/routes/team.ts +++ b/src/routes/team.ts @@ -1,36 +1,38 @@ -import { OpenAPIHono, createRoute } from "@hono/zod-openapi"; -import type { Env } from "../types"; -import { ErrorResponseSchema } from "../schemas/common"; -import { TeamResponseSchema } from "../schemas/contributors"; -import * as teamService from "../services/team"; -import { cacheControl, CacheDuration } from "../cache"; +import { OpenAPIHono, createRoute } from '@hono/zod-openapi'; +import type { Env } from '../types'; +import { ErrorResponseSchema } from '../schemas/common'; +import { TeamResponseSchema } from '../schemas/contributors'; +import * as teamService from '../services/team'; +import { cacheControl, CacheDuration } from '../cache'; const app = new OpenAPIHono<{ Bindings: Env }>(); // 1-day cache for team members -app.use("*", cacheControl(CacheDuration.day)); +app.use('*', cacheControl(CacheDuration.day)); app.openapi( - createRoute({ - method: "get", - path: "/", - tags: ["API"], - summary: "Get team members", - description: "Get the list of team members from the organization.", - responses: { - 200: { - content: { "application/json": { schema: TeamResponseSchema } }, - description: "The list of team members.", - }, - 500: { - content: { "application/json": { schema: ErrorResponseSchema } }, - description: "GitHub API error.", - }, - }, - }), - async (c) => { - return c.json(await teamService.getTeamMembers(c.env), 200); - }, + createRoute({ + method: 'get', + path: '/', + tags: ['API'], + summary: 'Get team members', + description: 'Get the list of team members from the organization.', + responses: { + 200: { + content: { 'application/json': { schema: TeamResponseSchema } }, + description: 'The list of team members.' + }, + 500: { + content: { + 'application/json': { schema: ErrorResponseSchema } + }, + description: 'GitHub API error.' + } + } + }), + async (c) => { + return c.json(await teamService.getTeamMembers(c.env), 200); + } ); export default app; diff --git a/src/schemas/announcements.ts b/src/schemas/announcements.ts index a2264d08..e0a7e92d 100644 --- a/src/schemas/announcements.ts +++ b/src/schemas/announcements.ts @@ -1,89 +1,89 @@ -import { z } from "@hono/zod-openapi"; +import { z } from '@hono/zod-openapi'; export const AnnouncementIdParamSchema = z.object({ - id: z.coerce - .number() - .int() - .openapi({ - description: "Announcement ID.", - example: 1, - param: { in: "path", required: true }, - }), + id: z.coerce + .number() + .int() + .openapi({ + description: 'Announcement ID.', + example: 1, + param: { in: 'path', required: true } + }) }); export const AnnouncementResponseSchema = z - .object({ - id: z.number().int().openapi({ example: 1 }), - author: z.string().nullable().openapi({ example: "ReVanced" }), - title: z.string().openapi({ example: "Welcome" }), - content: z.string().nullable().openapi({ example: "Some content" }), - tags: z.array(z.string()).openapi({ example: ["Important"] }), - created_at: z.iso - .datetime() - .openapi({ example: "1970-01-01T00:00:00.000Z" }), - archived_at: z.iso.datetime().nullable().openapi({ example: null }), - level: z.number().int().openapi({ example: 0 }), - }) - .openapi("Announcement"); + .object({ + id: z.number().int().openapi({ example: 1 }), + author: z.string().nullable().openapi({ example: 'ReVanced' }), + title: z.string().openapi({ example: 'Welcome' }), + content: z.string().nullable().openapi({ example: 'Some content' }), + tags: z.array(z.string()).openapi({ example: ['Important'] }), + created_at: z.iso + .datetime() + .openapi({ example: '1970-01-01T00:00:00.000Z' }), + archived_at: z.iso.datetime().nullable().openapi({ example: null }), + level: z.number().int().openapi({ example: 0 }) + }) + .openapi('Announcement'); export const AnnouncementsResponseSchema = z.array(AnnouncementResponseSchema); export const LatestAnnouncementEntrySchema = z - .object({ - tag: z.string().nullable().openapi({ example: "Important" }), - announcement: AnnouncementResponseSchema, - }) - .openapi("LatestAnnouncementEntry"); + .object({ + tag: z.string().nullable().openapi({ example: 'Important' }), + announcement: AnnouncementResponseSchema + }) + .openapi('LatestAnnouncementEntry'); export const LatestAnnouncementsResponseSchema = z - .array(LatestAnnouncementEntrySchema) - .openapi("LatestAnnouncementsByTag"); + .array(LatestAnnouncementEntrySchema) + .openapi('LatestAnnouncementsByTag'); export const LatestAnnouncementIdEntrySchema = z - .object({ - tag: z.string().nullable().openapi({ example: "Important" }), - id: z.number().int().openapi({ example: 1 }), - }) - .openapi("LatestAnnouncementIdEntry"); + .object({ + tag: z.string().nullable().openapi({ example: 'Important' }), + id: z.number().int().openapi({ example: 1 }) + }) + .openapi('LatestAnnouncementIdEntry'); export const LatestAnnouncementIdsResponseSchema = z - .array(LatestAnnouncementIdEntrySchema) - .openapi("LatestAnnouncementIdsByTag"); + .array(LatestAnnouncementIdEntrySchema) + .openapi('LatestAnnouncementIdsByTag'); export const CreateAnnouncementBodySchema = z - .object({ - author: z.string().optional().openapi({ example: "ReVanced" }), - title: z.string().openapi({ example: "Welcome" }), - content: z.string().optional().openapi({ example: "Some content" }), - tags: z.array(z.string()).openapi({ example: ["Important"] }), - created_at: z.string().datetime().nullable().optional().openapi({ - example: "1970-01-01T00:00:00.000Z", - description: "UTC timestamp. Defaults to current time if omitted.", - }), - archived_at: z.iso - .datetime() - .nullable() - .optional() - .openapi({ example: null, description: "UTC timestamp." }), - level: z.number().int().optional().default(0).openapi({ example: 0 }), - }) - .openapi("CreateAnnouncement"); + .object({ + author: z.string().optional().openapi({ example: 'ReVanced' }), + title: z.string().openapi({ example: 'Welcome' }), + content: z.string().optional().openapi({ example: 'Some content' }), + tags: z.array(z.string()).openapi({ example: ['Important'] }), + created_at: z.string().datetime().nullable().optional().openapi({ + example: '1970-01-01T00:00:00.000Z', + description: 'UTC timestamp. Defaults to current time if omitted.' + }), + archived_at: z.iso + .datetime() + .nullable() + .optional() + .openapi({ example: null, description: 'UTC timestamp.' }), + level: z.number().int().optional().default(0).openapi({ example: 0 }) + }) + .openapi('CreateAnnouncement'); export const UpdateAnnouncementBodySchema = z - .object({ - author: z.string().optional().openapi({ example: "ReVanced" }), - title: z.string().openapi({ example: "Welcome" }), - content: z.string().optional().openapi({ example: "Some content" }), - tags: z.array(z.string()).openapi({ example: ["Important"] }), - created_at: z.iso.datetime().nullable().optional().openapi({ - example: "1970-01-01T00:00:00.000Z", - description: "UTC timestamp.", - }), - archived_at: z.iso - .datetime() - .nullable() - .optional() - .openapi({ example: null, description: "UTC timestamp." }), - level: z.number().int().optional().default(0).openapi({ example: 0 }), - }) - .openapi("UpdateAnnouncement"); + .object({ + author: z.string().optional().openapi({ example: 'ReVanced' }), + title: z.string().openapi({ example: 'Welcome' }), + content: z.string().optional().openapi({ example: 'Some content' }), + tags: z.array(z.string()).openapi({ example: ['Important'] }), + created_at: z.iso.datetime().nullable().optional().openapi({ + example: '1970-01-01T00:00:00.000Z', + description: 'UTC timestamp.' + }), + archived_at: z.iso + .datetime() + .nullable() + .optional() + .openapi({ example: null, description: 'UTC timestamp.' }), + level: z.number().int().optional().default(0).openapi({ example: 0 }) + }) + .openapi('UpdateAnnouncement'); diff --git a/src/schemas/common.ts b/src/schemas/common.ts index 8306c750..d2629cec 100644 --- a/src/schemas/common.ts +++ b/src/schemas/common.ts @@ -1,5 +1,5 @@ -import { z } from "@hono/zod-openapi"; +import { z } from '@hono/zod-openapi'; export const ErrorResponseSchema = z.object({ - error: z.string().openapi({ example: "Something went wrong" }), + error: z.string().openapi({ example: 'Something went wrong' }) }); diff --git a/src/schemas/contributors.ts b/src/schemas/contributors.ts index a83630d0..247130bd 100644 --- a/src/schemas/contributors.ts +++ b/src/schemas/contributors.ts @@ -1,37 +1,47 @@ -import { z } from "@hono/zod-openapi"; +import { z } from '@hono/zod-openapi'; export const ContributorSchema = z.object({ - name: z.string().openapi({ example: "oSumAtrIX" }), - avatar_url: z.url().openapi({ example: "https://avatars.githubusercontent.com/u/..." }), - url: z.url().openapi({ example: "https://github.com/oSumAtrIX" }), - contributions: z.number().int().openapi({ example: 542 }), + name: z.string().openapi({ example: 'oSumAtrIX' }), + avatar_url: z + .url() + .openapi({ example: 'https://avatars.githubusercontent.com/u/...' }), + url: z.url().openapi({ example: 'https://github.com/oSumAtrIX' }), + contributions: z.number().int().openapi({ example: 542 }) }); export const ContributableSchema = z - .object({ - name: z.string().openapi({ example: "ReVanced Patches" }), - url: z.url().openapi({ example: "https://github.com/revanced/revanced-patches" }), - contributors: z.array(ContributorSchema), - }) - .openapi("Contributable"); + .object({ + name: z.string().openapi({ example: 'ReVanced Patches' }), + url: z + .url() + .openapi({ + example: 'https://github.com/revanced/revanced-patches' + }), + contributors: z.array(ContributorSchema) + }) + .openapi('Contributable'); export const ContributorsResponseSchema = z.array(ContributableSchema); export const GpgKeySchema = z - .object({ - id: z.string().openapi({ example: "ABC123DEF456" }), - url: z.url().openapi({ example: "https://github.com/oSumAtrIX.gpg" }), - }) - .nullable(); + .object({ + id: z.string().openapi({ example: 'ABC123DEF456' }), + url: z.url().openapi({ example: 'https://github.com/oSumAtrIX.gpg' }) + }) + .nullable(); export const TeamMemberSchema = z - .object({ - name: z.string().openapi({ example: "oSumAtrIX" }), - avatar_url: z.url().openapi({ example: "https://avatars.githubusercontent.com/u/..." }), - url: z.url().openapi({ example: "https://github.com/oSumAtrIX" }), - bio: z.string().nullable().openapi({ example: "Some bio text" }), - gpg_key: GpgKeySchema, - }) - .openapi("TeamMember"); + .object({ + name: z.string().openapi({ example: 'oSumAtrIX' }), + avatar_url: z + .url() + .openapi({ + example: 'https://avatars.githubusercontent.com/u/...' + }), + url: z.url().openapi({ example: 'https://github.com/oSumAtrIX' }), + bio: z.string().nullable().openapi({ example: 'Some bio text' }), + gpg_key: GpgKeySchema + }) + .openapi('TeamMember'); export const TeamResponseSchema = z.array(TeamMemberSchema); diff --git a/src/schemas/releases.ts b/src/schemas/releases.ts index 83d7dd8d..1a37ef23 100644 --- a/src/schemas/releases.ts +++ b/src/schemas/releases.ts @@ -1,47 +1,51 @@ -import { z } from "@hono/zod-openapi"; +import { z } from '@hono/zod-openapi'; export const ReleaseResponseSchema = z - .object({ - version: z.string().openapi({ example: "v4.0.0" }), - created_at: z.iso.datetime().openapi({ example: "1970-01-01T00:00:00.000Z" }), - description: z.string().openapi({ example: "Release notes markdown here..." }), - download_url: z.string().url().openapi({ - example: - "https://github.com/revanced/revanced-patches/releases/download/v4.0.0/patches-4.0.0.rvp", - }), - signature_download_url: z - .string() - .url() - .nullable() - .optional() - .openapi({ - example: - "https://github.com/revanced/revanced-patches/releases/download/v4.0.0/patches-4.0.0.rvp.asc", - }), - }) - .openapi("Release"); + .object({ + version: z.string().openapi({ example: 'v4.0.0' }), + created_at: z.iso + .datetime() + .openapi({ example: '1970-01-01T00:00:00.000Z' }), + description: z + .string() + .openapi({ example: 'Release notes markdown here...' }), + download_url: z.string().url().openapi({ + example: + 'https://github.com/revanced/revanced-patches/releases/download/v4.0.0/patches-4.0.0.rvp' + }), + signature_download_url: z.string().url().nullable().optional().openapi({ + example: + 'https://github.com/revanced/revanced-patches/releases/download/v4.0.0/patches-4.0.0.rvp.asc' + }) + }) + .openapi('Release'); export const VersionResponseSchema = z - .object({ - version: z.string().openapi({ example: "v4.0.0" }), - }) - .openapi("Version"); + .object({ + version: z.string().openapi({ example: 'v4.0.0' }) + }) + .openapi('Version'); export const ReleaseSimpleSchema = z - .object({ - version: z.string().openapi({ example: "v4.0.0" }), - created_at: z.iso.datetime().openapi({ example: "1970-01-01T00:00:00.000Z" }), - description: z.string().openapi({ example: "Release notes markdown here..." }), - }) - .openapi("ReleaseSimple"); + .object({ + version: z.string().openapi({ example: 'v4.0.0' }), + created_at: z.iso + .datetime() + .openapi({ example: '1970-01-01T00:00:00.000Z' }), + description: z + .string() + .openapi({ example: 'Release notes markdown here...' }) + }) + .openapi('ReleaseSimple'); export const HistoryResponseSchema = z.array(ReleaseSimpleSchema); export const PublicKeyResponseSchema = z - .object({ - patches_public_key: z.string().openapi({ - example: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----", - description: "The PGP public key for verifying patches assets.", - }), - }) - .openapi("PublicKey"); + .object({ + patches_public_key: z.string().openapi({ + example: + '-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----', + description: 'The PGP public key for verifying patches assets.' + }) + }) + .openapi('PublicKey'); diff --git a/src/services/announcements.ts b/src/services/announcements.ts index 5ab2a744..fc4f3a65 100644 --- a/src/services/announcements.ts +++ b/src/services/announcements.ts @@ -1,349 +1,351 @@ -import { getDatabase } from "../db/client"; -import { announcements, tags, announcementTags } from "../db/schema"; -import { eq, desc, and, count, inArray, isNull } from "drizzle-orm"; -import type { Env } from "../types"; +import { getDatabase } from '../db/client'; +import { announcements, tags, announcementTags } from '../db/schema'; +import { eq, desc, and, count, inArray, isNull } from 'drizzle-orm'; +import type { Env } from '../types'; async function getTagsForAnnouncement( - db: ReturnType, - announcementId: number, + db: ReturnType, + announcementId: number ): Promise { - const rows = await db - .select({ name: tags.name }) - .from(announcementTags) - .innerJoin(tags, eq(announcementTags.tagId, tags.id)) - .where(eq(announcementTags.announcementId, announcementId)); - return rows.map((r) => r.name); + const rows = await db + .select({ name: tags.name }) + .from(announcementTags) + .innerJoin(tags, eq(announcementTags.tagId, tags.id)) + .where(eq(announcementTags.announcementId, announcementId)); + return rows.map((r) => r.name); } async function getTagsForAnnouncements( - db: ReturnType, - announcementIds: number[], + db: ReturnType, + announcementIds: number[] ): Promise> { - if (announcementIds.length === 0) { - return new Map(); - } - - const rows = await db - .select({ - announcementId: announcementTags.announcementId, - name: tags.name, - }) - .from(announcementTags) - .innerJoin(tags, eq(announcementTags.tagId, tags.id)) - .where(inArray(announcementTags.announcementId, announcementIds)); - - const tagMap = new Map(); - for (const row of rows) { - const announcementTagNames = tagMap.get(row.announcementId); - if (announcementTagNames) { - announcementTagNames.push(row.name); - continue; - } - - tagMap.set(row.announcementId, [row.name]); - } - - return tagMap; + if (announcementIds.length === 0) { + return new Map(); + } + + const rows = await db + .select({ + announcementId: announcementTags.announcementId, + name: tags.name + }) + .from(announcementTags) + .innerJoin(tags, eq(announcementTags.tagId, tags.id)) + .where(inArray(announcementTags.announcementId, announcementIds)); + + const tagMap = new Map(); + for (const row of rows) { + const announcementTagNames = tagMap.get(row.announcementId); + if (announcementTagNames) { + announcementTagNames.push(row.name); + continue; + } + + tagMap.set(row.announcementId, [row.name]); + } + + return tagMap; } async function getLatestAnnouncementRowsByTag( - db: ReturnType, + db: ReturnType ) { - const rows = await db - .select({ tagName: tags.name, announcement: announcements }) - .from(tags) - .innerJoin(announcementTags, eq(announcementTags.tagId, tags.id)) - .innerJoin( - announcements, - eq(announcementTags.announcementId, announcements.id), - ) - .orderBy(tags.name, desc(announcements.id)); - - const latestByTag = new Map(); - for (const row of rows) { - if (!latestByTag.has(row.tagName)) { - latestByTag.set(row.tagName, row.announcement); - } - } - - return latestByTag; + const rows = await db + .select({ tagName: tags.name, announcement: announcements }) + .from(tags) + .innerJoin(announcementTags, eq(announcementTags.tagId, tags.id)) + .innerJoin( + announcements, + eq(announcementTags.announcementId, announcements.id) + ) + .orderBy(tags.name, desc(announcements.id)); + + const latestByTag = new Map(); + for (const row of rows) { + if (!latestByTag.has(row.tagName)) { + latestByTag.set(row.tagName, row.announcement); + } + } + + return latestByTag; } async function getLatestUntaggedAnnouncementRow( - db: ReturnType, + db: ReturnType ) { - const [row] = await db - .select({ announcement: announcements }) - .from(announcements) - .leftJoin( - announcementTags, - eq(announcementTags.announcementId, announcements.id), - ) - .where(isNull(announcementTags.announcementId)) - .orderBy(desc(announcements.id)) - .limit(1); - - return row?.announcement ?? null; + const [row] = await db + .select({ announcement: announcements }) + .from(announcements) + .leftJoin( + announcementTags, + eq(announcementTags.announcementId, announcements.id) + ) + .where(isNull(announcementTags.announcementId)) + .orderBy(desc(announcements.id)) + .limit(1); + + return row?.announcement ?? null; } /** Find or create a tag by name, returning its ID. */ async function findOrCreateTag( - db: ReturnType, - name: string, + db: ReturnType, + name: string ): Promise { - await db.insert(tags).values({ name }).onConflictDoNothing(); - const [row] = await db - .select({ id: tags.id }) - .from(tags) - .where(eq(tags.name, name)); - return row.id; + await db.insert(tags).values({ name }).onConflictDoNothing(); + const [row] = await db + .select({ id: tags.id }) + .from(tags) + .where(eq(tags.name, name)); + return row.id; } function formatRow( - row: typeof announcements.$inferSelect, - announcementTagNames: string[] = [], + row: typeof announcements.$inferSelect, + announcementTagNames: string[] = [] ) { - return { - id: row.id, - author: row.author, - title: row.title, - content: row.content, - tags: announcementTagNames, - created_at: row.createdAt, - archived_at: row.archivedAt, - level: row.level, - }; + return { + id: row.id, + author: row.author, + title: row.title, + content: row.content, + tags: announcementTagNames, + created_at: row.createdAt, + archived_at: row.archivedAt, + level: row.level + }; } type LatestAnnouncementEntry = { - tag: string | null; - announcement: ReturnType; + tag: string | null; + announcement: ReturnType; }; type LatestAnnouncementIdEntry = { - tag: string | null; - id: number; + tag: string | null; + id: number; }; export async function listAnnouncements(env: Env) { - const database = getDatabase(env.DB); - const rows = await database - .select() - .from(announcements) - .orderBy(desc(announcements.id)); - - const results = []; - for (const row of rows) { - const tagNames = await getTagsForAnnouncement(database, row.id); - results.push(formatRow(row, tagNames)); - } - return results; + const database = getDatabase(env.DB); + const rows = await database + .select() + .from(announcements) + .orderBy(desc(announcements.id)); + + const results = []; + for (const row of rows) { + const tagNames = await getTagsForAnnouncement(database, row.id); + results.push(formatRow(row, tagNames)); + } + return results; } export async function getLatestAnnouncementsByTag(env: Env) { - const database = getDatabase(env.DB); - const latestByTag = await getLatestAnnouncementRowsByTag(database); - const latestUntagged = await getLatestUntaggedAnnouncementRow(database); - const announcementIds = [ - ...new Set([...latestByTag.values()].map((row) => row.id)), - ]; - if (latestUntagged) { - announcementIds.push(latestUntagged.id); - } - const announcementTagsMap = await getTagsForAnnouncements(database, [ - ...new Set(announcementIds), - ]); - - const entries: LatestAnnouncementEntry[] = [...latestByTag.entries()].map( - ([tagName, row]) => ({ - tag: tagName, - announcement: formatRow(row, announcementTagsMap.get(row.id) ?? []), - }), - ); - - if (latestUntagged) { - entries.push({ - tag: null, - announcement: formatRow(latestUntagged, []), - }); - } - - return entries; + const database = getDatabase(env.DB); + const latestByTag = await getLatestAnnouncementRowsByTag(database); + const latestUntagged = await getLatestUntaggedAnnouncementRow(database); + const announcementIds = [ + ...new Set([...latestByTag.values()].map((row) => row.id)) + ]; + if (latestUntagged) { + announcementIds.push(latestUntagged.id); + } + const announcementTagsMap = await getTagsForAnnouncements(database, [ + ...new Set(announcementIds) + ]); + + const entries: LatestAnnouncementEntry[] = [...latestByTag.entries()].map( + ([tagName, row]) => ({ + tag: tagName, + announcement: formatRow(row, announcementTagsMap.get(row.id) ?? []) + }) + ); + + if (latestUntagged) { + entries.push({ + tag: null, + announcement: formatRow(latestUntagged, []) + }); + } + + return entries; } export async function getLatestAnnouncementIdsByTag(env: Env) { - const database = getDatabase(env.DB); - const latestByTag = await getLatestAnnouncementRowsByTag(database); - const latestUntagged = await getLatestUntaggedAnnouncementRow(database); - - const entries: LatestAnnouncementIdEntry[] = [...latestByTag.entries()].map( - ([tagName, row]) => ({ - tag: tagName, - id: row.id, - }), - ); - - if (latestUntagged) { - entries.push({ tag: null, id: latestUntagged.id }); - } - - return entries; + const database = getDatabase(env.DB); + const latestByTag = await getLatestAnnouncementRowsByTag(database); + const latestUntagged = await getLatestUntaggedAnnouncementRow(database); + + const entries: LatestAnnouncementIdEntry[] = [...latestByTag.entries()].map( + ([tagName, row]) => ({ + tag: tagName, + id: row.id + }) + ); + + if (latestUntagged) { + entries.push({ tag: null, id: latestUntagged.id }); + } + + return entries; } export async function createAnnouncement( - env: Env, - body: { - author?: string; - title: string; - content?: string; - created_at?: string | null; - tags?: string[]; - level?: number; - }, + env: Env, + body: { + author?: string; + title: string; + content?: string; + created_at?: string | null; + tags?: string[]; + level?: number; + } ) { - const database = getDatabase(env.DB); - const result = await database - .insert(announcements) - .values({ - author: body.author ?? null, - title: body.title, - content: body.content ?? null, - createdAt: body.created_at ?? new Date().toISOString(), - level: body.level ?? 0, - }) - .returning(); - - const created = result[0]; - - if (body.tags && body.tags.length > 0) { - const tagIds: number[] = []; - for (const name of body.tags) { - tagIds.push(await findOrCreateTag(database, name)); - } - await database - .insert(announcementTags) - .values(tagIds.map((tagId) => ({ announcementId: created.id, tagId }))); - } - - const tagNames = await getTagsForAnnouncement(database, created.id); - return formatRow(created, tagNames); + const database = getDatabase(env.DB); + const result = await database + .insert(announcements) + .values({ + author: body.author ?? null, + title: body.title, + content: body.content ?? null, + createdAt: body.created_at ?? new Date().toISOString(), + level: body.level ?? 0 + }) + .returning(); + + const created = result[0]; + + if (body.tags && body.tags.length > 0) { + const tagIds: number[] = []; + for (const name of body.tags) { + tagIds.push(await findOrCreateTag(database, name)); + } + await database + .insert(announcementTags) + .values( + tagIds.map((tagId) => ({ announcementId: created.id, tagId })) + ); + } + + const tagNames = await getTagsForAnnouncement(database, created.id); + return formatRow(created, tagNames); } export async function updateAnnouncement( - env: Env, - id: number, - body: { - author?: string; - title?: string | null; - content?: string | null; - created_at?: string | null; - tags?: string[]; - archived_at?: string | null; - level?: number | null; - }, + env: Env, + id: number, + body: { + author?: string; + title?: string | null; + content?: string | null; + created_at?: string | null; + tags?: string[]; + archived_at?: string | null; + level?: number | null; + } ) { - const database = getDatabase(env.DB); - - const updates: Record = {}; - if (body.author) updates.author = body.author; - if (body.title) updates.title = body.title; - if (body.content) updates.content = body.content; - if (body.created_at) updates.createdAt = body.created_at; - if (body.archived_at) updates.archivedAt = body.archived_at; - if (body.level) updates.level = body.level; - - let row; - if (Object.keys(updates).length > 0) { - const result = await database - .update(announcements) - .set(updates) - .where(eq(announcements.id, id)) - .returning(); - if (result.length === 0) return null; - row = result[0]; - } else { - const rows = await database - .select() - .from(announcements) - .where(eq(announcements.id, id)); - if (rows.length === 0) return null; - row = rows[0]; - } - - if (body.tags !== undefined) { - // Get current tags for this announcement - const currentTagRows = await database - .select({ tagId: announcementTags.tagId, name: tags.name }) - .from(announcementTags) - .innerJoin(tags, eq(announcementTags.tagId, tags.id)) - .where(eq(announcementTags.announcementId, id)); - - const currentTagNames = new Set(currentTagRows.map((r) => r.name)); - const newTagNames = new Set(body.tags); - - // Add tags that are in the new set but not currently associated - for (const name of body.tags) { - if (!currentTagNames.has(name)) { - const tagId = await findOrCreateTag(database, name); - await database - .insert(announcementTags) - .values({ announcementId: id, tagId }); - } - } - - // Remove tags no longer needed and delete orphaned tag records - for (const { tagId, name } of currentTagRows) { - if (newTagNames.has(name)) continue; - - await database - .delete(announcementTags) - .where( - and( - eq(announcementTags.announcementId, id), - eq(announcementTags.tagId, tagId), - ), - ); - - const [usage] = await database - .select({ count: count() }) - .from(announcementTags) - .where(eq(announcementTags.tagId, tagId)); - if (usage.count === 0) { - await database.delete(tags).where(eq(tags.id, tagId)); - } - } - } - - const tagNames = await getTagsForAnnouncement(database, id); - return formatRow(row, tagNames); + const database = getDatabase(env.DB); + + const updates: Record = {}; + if (body.author) updates.author = body.author; + if (body.title) updates.title = body.title; + if (body.content) updates.content = body.content; + if (body.created_at) updates.createdAt = body.created_at; + if (body.archived_at) updates.archivedAt = body.archived_at; + if (body.level) updates.level = body.level; + + let row; + if (Object.keys(updates).length > 0) { + const result = await database + .update(announcements) + .set(updates) + .where(eq(announcements.id, id)) + .returning(); + if (result.length === 0) return null; + row = result[0]; + } else { + const rows = await database + .select() + .from(announcements) + .where(eq(announcements.id, id)); + if (rows.length === 0) return null; + row = rows[0]; + } + + if (body.tags !== undefined) { + // Get current tags for this announcement + const currentTagRows = await database + .select({ tagId: announcementTags.tagId, name: tags.name }) + .from(announcementTags) + .innerJoin(tags, eq(announcementTags.tagId, tags.id)) + .where(eq(announcementTags.announcementId, id)); + + const currentTagNames = new Set(currentTagRows.map((r) => r.name)); + const newTagNames = new Set(body.tags); + + // Add tags that are in the new set but not currently associated + for (const name of body.tags) { + if (!currentTagNames.has(name)) { + const tagId = await findOrCreateTag(database, name); + await database + .insert(announcementTags) + .values({ announcementId: id, tagId }); + } + } + + // Remove tags no longer needed and delete orphaned tag records + for (const { tagId, name } of currentTagRows) { + if (newTagNames.has(name)) continue; + + await database + .delete(announcementTags) + .where( + and( + eq(announcementTags.announcementId, id), + eq(announcementTags.tagId, tagId) + ) + ); + + const [usage] = await database + .select({ count: count() }) + .from(announcementTags) + .where(eq(announcementTags.tagId, tagId)); + if (usage.count === 0) { + await database.delete(tags).where(eq(tags.id, tagId)); + } + } + } + + const tagNames = await getTagsForAnnouncement(database, id); + return formatRow(row, tagNames); } export async function deleteAnnouncement(env: Env, id: number) { - const database = getDatabase(env.DB); - - // Get tags associated with this announcement before deleting - const announcementTagRows = await database - .select({ tagId: announcementTags.tagId }) - .from(announcementTags) - .where(eq(announcementTags.announcementId, id)); - const tagIds = announcementTagRows.map((r) => r.tagId); - - const result = await database - .delete(announcements) - .where(eq(announcements.id, id)) - .returning(); - if (result.length === 0) return false; - - // Delete tags that are no longer referenced by any announcement - for (const tagId of tagIds) { - const [usage] = await database - .select({ count: count() }) - .from(announcementTags) - .where(eq(announcementTags.tagId, tagId)); - if (usage.count === 0) { - await database.delete(tags).where(eq(tags.id, tagId)); - } - } - - return true; + const database = getDatabase(env.DB); + + // Get tags associated with this announcement before deleting + const announcementTagRows = await database + .select({ tagId: announcementTags.tagId }) + .from(announcementTags) + .where(eq(announcementTags.announcementId, id)); + const tagIds = announcementTagRows.map((r) => r.tagId); + + const result = await database + .delete(announcements) + .where(eq(announcements.id, id)) + .returning(); + if (result.length === 0) return false; + + // Delete tags that are no longer referenced by any announcement + for (const tagId of tagIds) { + const [usage] = await database + .select({ count: count() }) + .from(announcementTags) + .where(eq(announcementTags.tagId, tagId)); + if (usage.count === 0) { + await database.delete(tags).where(eq(tags.id, tagId)); + } + } + + return true; } diff --git a/src/services/contributors.ts b/src/services/contributors.ts index 58154b7c..b8e99a7d 100644 --- a/src/services/contributors.ts +++ b/src/services/contributors.ts @@ -1,25 +1,25 @@ -import { getBackend, getConfig } from "../config"; -import type { Env } from "../types"; +import { getBackend, getConfig } from '../config'; +import type { Env } from '../types'; export async function getContributors(env: Env) { - const backend = getBackend(env); - const { organization, contributorRepos } = getConfig(env); + const backend = getBackend(env); + const { organization, contributorRepos } = getConfig(env); - const results = await Promise.all( - contributorRepos.map(async ({ repo, name }) => { - const contributors = await backend.contributors(organization, repo); - return { - name, - url: backend.repositoryUrl(organization, repo), - contributors: contributors.map((contributor) => ({ - name: contributor.name, - avatar_url: contributor.avatarUrl, - url: contributor.url, - contributions: contributor.contributions, - })), - }; - }), - ); + const results = await Promise.all( + contributorRepos.map(async ({ repo, name }) => { + const contributors = await backend.contributors(organization, repo); + return { + name, + url: backend.repositoryUrl(organization, repo), + contributors: contributors.map((contributor) => ({ + name: contributor.name, + avatar_url: contributor.avatarUrl, + url: contributor.url, + contributions: contributor.contributions + })) + }; + }) + ); - return results; + return results; } diff --git a/src/services/manager.ts b/src/services/manager.ts index e61f4e5c..04b5ce7d 100644 --- a/src/services/manager.ts +++ b/src/services/manager.ts @@ -1,64 +1,84 @@ -import { getBackend, getConfig } from "../config"; -import type { Env } from "../types"; +import { getBackend, getConfig } from '../config'; +import type { Env } from '../types'; export async function getRelease(env: Env, prerelease: boolean) { - const backend = getBackend(env); - const { organization, manager } = getConfig(env); + const backend = getBackend(env); + const { organization, manager } = getConfig(env); - const release = await backend.release(organization, manager.repo, prerelease); - const asset = release.assets.find((item) => manager.assetRegex.test(item.name)); + const release = await backend.release( + organization, + manager.repo, + prerelease + ); + const asset = release.assets.find((item) => + manager.assetRegex.test(item.name) + ); - return { - version: release.tag, - created_at: release.createdAt, - description: release.releaseNote, - download_url: asset?.downloadUrl ?? "", - }; + return { + version: release.tag, + created_at: release.createdAt, + description: release.releaseNote, + download_url: asset?.downloadUrl ?? '' + }; } export async function getVersion(env: Env, prerelease: boolean) { - const backend = getBackend(env); - const { organization, manager } = getConfig(env); + const backend = getBackend(env); + const { organization, manager } = getConfig(env); - const release = await backend.release(organization, manager.repo, prerelease); - return { version: release.tag }; + const release = await backend.release( + organization, + manager.repo, + prerelease + ); + return { version: release.tag }; } export async function getHistory(env: Env, prerelease: boolean) { - const backend = getBackend(env); - const { organization, manager } = getConfig(env); + const backend = getBackend(env); + const { organization, manager } = getConfig(env); - const allReleases = await backend.releases(organization, manager.repo, 100); - const filtered = prerelease - ? allReleases - : allReleases.filter((r) => !r.prerelease); + const allReleases = await backend.releases(organization, manager.repo, 100); + const filtered = prerelease + ? allReleases + : allReleases.filter((r) => !r.prerelease); - return filtered.map((r) => ({ - version: r.tag, - created_at: r.createdAt, - description: r.releaseNote, - })); + return filtered.map((r) => ({ + version: r.tag, + created_at: r.createdAt, + description: r.releaseNote + })); } export async function getDownloadersRelease(env: Env, prerelease: boolean) { - const backend = getBackend(env); - const { organization, manager } = getConfig(env); + const backend = getBackend(env); + const { organization, manager } = getConfig(env); - const release = await backend.release(organization, manager.downloadersRepo, prerelease); - const asset = release.assets.find((item) => manager.downloadersAssetRegex.test(item.name)); + const release = await backend.release( + organization, + manager.downloadersRepo, + prerelease + ); + const asset = release.assets.find((item) => + manager.downloadersAssetRegex.test(item.name) + ); - return { - version: release.tag, - created_at: release.createdAt, - description: release.releaseNote, - download_url: asset?.downloadUrl ?? "", - }; + return { + version: release.tag, + created_at: release.createdAt, + description: release.releaseNote, + download_url: asset?.downloadUrl ?? '' + }; } export async function getDownloadersVersion(env: Env, prerelease: boolean) { - const backend = getBackend(env); - const { organization, manager } = getConfig(env); + const backend = getBackend(env); + const { organization, manager } = getConfig(env); - const release = await backend.release(organization, manager.downloadersRepo, prerelease); - return { version: release.tag }; + const release = await backend.release( + organization, + manager.downloadersRepo, + prerelease + ); + return { version: release.tag }; } diff --git a/src/services/patches.ts b/src/services/patches.ts index 2f55be6a..8566570f 100644 --- a/src/services/patches.ts +++ b/src/services/patches.ts @@ -1,65 +1,73 @@ -import { getBackend, getConfig } from "../config"; -import type { Env } from "../types"; +import { getBackend, getConfig } from '../config'; +import type { Env } from '../types'; export async function getRelease(env: Env, prerelease: boolean) { - const backend = getBackend(env); - const { organization, patches } = getConfig(env); + const backend = getBackend(env); + const { organization, patches } = getConfig(env); - const release = await backend.release(organization, patches.repo, prerelease); - const asset = release.assets.find((item) => - patches.assetRegex.test(item.name), - ); - const signatureAsset = release.assets.find((item) => - patches.signatureAssetRegex.test(item.name), - ); + const release = await backend.release( + organization, + patches.repo, + prerelease + ); + const asset = release.assets.find((item) => + patches.assetRegex.test(item.name) + ); + const signatureAsset = release.assets.find((item) => + patches.signatureAssetRegex.test(item.name) + ); - return { - version: release.tag, - created_at: release.createdAt, - description: release.releaseNote, - download_url: asset?.downloadUrl ?? "", - signature_download_url: signatureAsset?.downloadUrl ?? null, - }; + return { + version: release.tag, + created_at: release.createdAt, + description: release.releaseNote, + download_url: asset?.downloadUrl ?? '', + signature_download_url: signatureAsset?.downloadUrl ?? null + }; } export async function getVersion(env: Env, prerelease: boolean) { - const backend = getBackend(env); - const { organization, patches } = getConfig(env); + const backend = getBackend(env); + const { organization, patches } = getConfig(env); - const release = await backend.release(organization, patches.repo, prerelease); - return { version: release.tag }; + const release = await backend.release( + organization, + patches.repo, + prerelease + ); + return { version: release.tag }; } export async function getHistory(env: Env, prerelease: boolean) { - const backend = getBackend(env); - const { organization, patches } = getConfig(env); + const backend = getBackend(env); + const { organization, patches } = getConfig(env); - const allReleases = await backend.releases(organization, patches.repo, 100); - const filtered = prerelease - ? allReleases - : allReleases.filter((r) => !r.prerelease); + const allReleases = await backend.releases(organization, patches.repo, 100); + const filtered = prerelease + ? allReleases + : allReleases.filter((r) => !r.prerelease); - return filtered.map((r) => ({ - version: r.tag, - created_at: r.createdAt, - description: r.releaseNote, - })); + return filtered.map((r) => ({ + version: r.tag, + created_at: r.createdAt, + description: r.releaseNote + })); } let _publicKeyCache: string | undefined; export async function getPublicKey(env: Env) { - if (!_publicKeyCache) { - const { patches } = getConfig(env); - const res = await env.ASSETS.fetch( - new URL(patches.publicKeyFile, "https://assets.local"), - ); - if (!res.ok) { - throw new Error( - `Failed to load public key from ${patches.publicKeyFile}: ${res.status}`, - ); - } - _publicKeyCache = await res.text(); - } - return { patches_public_key: _publicKeyCache }; + if (!_publicKeyCache) { + const { patches } = getConfig(env); + const res = await env.ASSETS.fetch( + new URL(patches.publicKeyFile, 'https://assets.local') + ); + if (!res.ok) { + throw new Error( + `Failed to load public key from ${patches.publicKeyFile}: ${res.status}` + ); + } + _publicKeyCache = await res.text(); + } + return { patches_public_key: _publicKeyCache }; } diff --git a/src/services/team.ts b/src/services/team.ts index 5013895f..6bc8baa7 100644 --- a/src/services/team.ts +++ b/src/services/team.ts @@ -1,19 +1,19 @@ -import { getBackend, getConfig } from "../config"; -import type { Env } from "../types"; +import { getBackend, getConfig } from '../config'; +import type { Env } from '../types'; export async function getTeamMembers(env: Env) { - const backend = getBackend(env); - const { organization } = getConfig(env); + const backend = getBackend(env); + const { organization } = getConfig(env); - const members = await backend.members(organization); + const members = await backend.members(organization); - return members.map((member) => ({ - name: member.name, - avatar_url: member.avatarUrl, - url: member.url, - bio: member.bio, - gpg_key: member.gpgKeys.ids[0] - ? { id: member.gpgKeys.ids[0], url: member.gpgKeys.url } - : null, - })); + return members.map((member) => ({ + name: member.name, + avatar_url: member.avatarUrl, + url: member.url, + bio: member.bio, + gpg_key: member.gpgKeys.ids[0] + ? { id: member.gpgKeys.ids[0], url: member.gpgKeys.url } + : null + })); } diff --git a/src/types.ts b/src/types.ts index a43ef49f..e0052e08 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,22 +1,22 @@ -import type { DrizzleD1Database } from "drizzle-orm/d1"; -import type * as schema from "./db/schema"; +import type { DrizzleD1Database } from 'drizzle-orm/d1'; +import type * as schema from './db/schema'; export type Database = DrizzleD1Database; export interface Env { - ASSETS: Fetcher; - DB: D1Database; - API_TOKEN: string; - GITHUB_TOKEN?: string; - ORGANIZATION: string; - PATCHES_REPO: string; - PATCHES_ASSET_REGEX: string; - PATCHES_SIGNATURE_ASSET_REGEX: string; - MANAGER_REPO: string; - MANAGER_ASSET_REGEX: string; - MANAGER_DOWNLOADERS_REPO: string; - MANAGER_DOWNLOADERS_ASSET_REGEX: string; - PATCHES_PUBLIC_KEY_FILE: string; - CONTRIBUTORS_REPOS: string; - API_VERSION: string; + ASSETS: Fetcher; + DB: D1Database; + API_TOKEN: string; + GITHUB_TOKEN?: string; + ORGANIZATION: string; + PATCHES_REPO: string; + PATCHES_ASSET_REGEX: string; + PATCHES_SIGNATURE_ASSET_REGEX: string; + MANAGER_REPO: string; + MANAGER_ASSET_REGEX: string; + MANAGER_DOWNLOADERS_REPO: string; + MANAGER_DOWNLOADERS_ASSET_REGEX: string; + PATCHES_PUBLIC_KEY_FILE: string; + CONTRIBUTORS_REPOS: string; + API_VERSION: string; } diff --git a/tsconfig.json b/tsconfig.json index 0eb7c901..49c56984 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,22 @@ { - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "lib": ["ESNext"], - "types": ["@cloudflare/workers-types", "@types/node"], - "paths": { - "@/*": ["./src/*"] + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "lib": ["ESNext"], + "types": ["@cloudflare/workers-types", "@types/node"], + "paths": { + "@/*": ["./src/*"] + }, + "baseUrl": "." }, - "baseUrl": "." - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules"] + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] }