diff --git a/.vscode/settings.json b/.vscode/settings.json index b5a11ac9..487710fd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -37,5 +37,8 @@ }, "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" } } diff --git a/next.config.mjs b/next.config.mjs index 43208311..c6259af0 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -36,11 +36,15 @@ const withMDX = createMDX({ }); const nextConfig = { - pageExtensions: ['js', 'jsx', 'mdx', 'tsx'], + pageExtensions: ['js', 'ts', 'jsx', 'mdx', 'tsx'], transpilePackages: ['@mdxeditor/editor', 'react-diff-view'], swcMinify: false, poweredByHeader: false, reactStrictMode: true, + experimental: { + // Related to Pino error with RSC: https://github.com/orgs/vercel/discussions/3150 + serverComponentsExternalPackages: ['pino'], + }, images: { // limit of 25 deviceSizes values // deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], diff --git a/package-lock.json b/package-lock.json index 2ce1bb7c..6a5347a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,12 +26,16 @@ "@mui/material-nextjs": "^5.15.11", "@mui/utils": "^5.15.14", "@next/mdx": "^14.1.4", + "@octokit/auth-app": "^6.1.1", + "@octokit/rest": "^20.1.0", "@opensearch-project/opensearch": "^2.6.0", "@spotlightjs/spotlight": "^1.2.16", "@stefanprobst/rehype-extract-toc": "^2.2.0", "@t3-oss/env-nextjs": "^0.9.2", "gray-matter": "^4.0.3", + "ioredis": "^5.3.2", "langchain": "^0.1.31", + "mime-types": "^2.1.35", "next": "^14.1.4", "next-intl": "^3.10.0", "next-sitemap": "^4.2.3", @@ -71,6 +75,7 @@ "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.2", "@types/jest": "^29.5.12", + "@types/mime-types": "^2.1.4", "@types/node": "^20.12.3", "@types/react": "^18.2.74", "@typescript-eslint/eslint-plugin": "^7.5.0", @@ -3941,6 +3946,11 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -5867,6 +5877,256 @@ "node": ">= 8" } }, + "node_modules/@octokit/auth-app": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.1.1.tgz", + "integrity": "sha512-VrTtzRpyuT5nYGUWeGWQqH//hqEZDV+/yb6+w5wmWpmmUA1Tx950XsAc2mBBfvusfcdF2E7w8jZ1r1WwvfZ9pA==", + "dependencies": { + "@octokit/auth-oauth-app": "^7.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "deprecation": "^2.3.1", + "lru-cache": "^10.0.0", + "universal-github-app-jwt": "^1.1.2", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/endpoint": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", + "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/request": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", + "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", + "dependencies": { + "@octokit/endpoint": "^9.0.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/request-error": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", + "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/@octokit/auth-app/node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + }, + "node_modules/@octokit/auth-oauth-app": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.1.0.tgz", + "integrity": "sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "@types/btoa-lite": "^1.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/endpoint": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", + "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", + "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", + "dependencies": { + "@octokit/endpoint": "^9.0.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request-error": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", + "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + }, + "node_modules/@octokit/auth-oauth-device": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.1.0.tgz", + "integrity": "sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==", + "dependencies": { + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/endpoint": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", + "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", + "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", + "dependencies": { + "@octokit/endpoint": "^9.0.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request-error": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", + "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-device/node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + }, + "node_modules/@octokit/auth-oauth-user": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.1.0.tgz", + "integrity": "sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/endpoint": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", + "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", + "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", + "dependencies": { + "@octokit/endpoint": "^9.0.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request-error": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", + "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + }, "node_modules/@octokit/auth-token": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.0.tgz", @@ -5921,11 +6181,77 @@ "node": ">= 18" } }, + "node_modules/@octokit/oauth-authorization-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", + "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.1.0.tgz", + "integrity": "sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==", + "dependencies": { + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/endpoint": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", + "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/request": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", + "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", + "dependencies": { + "@octokit/endpoint": "^9.0.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/request-error": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", + "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + }, "node_modules/@octokit/openapi-types": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-21.2.0.tgz", - "integrity": "sha512-xx+Xd6I7rYvul/hgUDqv6TeGX0IOGnhSg9IOeYgd/uI7IAqUy6DE2B6Ipv2M4mWoxaMcWjIzgTIcv8pMO3F3vw==", - "dev": true + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.0.1.tgz", + "integrity": "sha512-1yN5m1IMNXthoBDUXFF97N1gHop04B3H8ws7wtOr8GgRyDO1gKALjwMHARNBoMBiB/2vEe/vxstrApcJZzQbnQ==" }, "node_modules/@octokit/plugin-paginate-rest": { "version": "10.0.0", @@ -6032,13 +6358,173 @@ "@octokit/openapi-types": "^20.0.0" } }, + "node_modules/@octokit/rest": { + "version": "20.1.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.0.tgz", + "integrity": "sha512-STVO3itHQLrp80lvcYB2UIKoeil5Ctsgd2s1AM+du3HqZIR35ZH7WE9HLwUOLXH0myA0y3AGNPo8gZtcgIbw0g==", + "dependencies": { + "@octokit/core": "^5.0.2", + "@octokit/plugin-paginate-rest": "^9.1.5", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-rest-endpoint-methods": "^10.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", + "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/endpoint": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", + "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/graphql": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.0.tgz", + "integrity": "sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==", + "dependencies": { + "@octokit/request": "^8.3.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==" + }, + "node_modules/@octokit/rest/node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.1.tgz", + "integrity": "sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw==", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/plugin-request-log": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz", + "integrity": "sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/request": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", + "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", + "dependencies": { + "@octokit/endpoint": "^9.0.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/request-error": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", + "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, + "node_modules/@octokit/rest/node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + }, "node_modules/@octokit/types": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.0.0.tgz", - "integrity": "sha512-jSOgEoFZvjg78txlb7cuRTAEvyyQkIEB4Nujg5ZN7E1xaICsr8A0X045Nwb1wUWNrBUHBHZNtcsDIhk8d8ipCw==", - "dev": true, + "version": "13.4.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.4.0.tgz", + "integrity": "sha512-WlMegy3lPXYWASe3k9Jslc5a0anrYAYMWtsFrxBTdQjS70hvLH6C+PGvHbOsgy3RA3LouGJoU/vAt4KarecQLQ==", "dependencies": { - "@octokit/openapi-types": "^21.0.0" + "@octokit/openapi-types": "^22.0.1" } }, "node_modules/@opensearch-project/opensearch": { @@ -9883,6 +10369,11 @@ "@types/node": "*" } }, + "node_modules/@types/btoa-lite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.2.tgz", + "integrity": "sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==" + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -10120,6 +10611,14 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/lodash": { "version": "4.17.0", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", @@ -10145,6 +10644,12 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", + "dev": true + }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -12361,6 +12866,11 @@ "node-int64": "^0.4.0" } }, + "node_modules/btoa-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", + "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==" + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -12393,6 +12903,11 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -13147,6 +13662,14 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -14774,6 +15297,14 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -14783,6 +15314,11 @@ "node": ">= 0.8" } }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -15865,6 +16401,14 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -20832,6 +21376,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ioredis": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", + "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", @@ -23904,6 +24471,27 @@ "node": "*" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -23919,6 +24507,25 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -24564,6 +25171,11 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", @@ -24576,17 +25188,40 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" }, "node_modules/lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "dev": true + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, "node_modules/lodash.kebabcase": { "version": "4.1.1", @@ -24618,6 +25253,11 @@ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lodash.snakecase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", @@ -33012,6 +33652,25 @@ "node": ">=8" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -34938,6 +35597,11 @@ "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", "dev": true }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/start-server-and-test": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.3.tgz", @@ -37481,6 +38145,15 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" }, + "node_modules/universal-github-app-jwt": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.1.2.tgz", + "integrity": "sha512-t1iB2FmLFE+yyJY9+3wMx0ejB+MQpEVkH0gQv7dR6FZyltyq+ZZO0uDpbopxhrZ3SLEO4dCEkIujOMldEQ2iOA==", + "dependencies": { + "@types/jsonwebtoken": "^9.0.0", + "jsonwebtoken": "^9.0.2" + } + }, "node_modules/universal-user-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", diff --git a/package.json b/package.json index ddf7ed73..ba815fa0 100644 --- a/package.json +++ b/package.json @@ -47,12 +47,16 @@ "@mui/material-nextjs": "^5.15.11", "@mui/utils": "^5.15.14", "@next/mdx": "^14.1.4", + "@octokit/auth-app": "^6.1.1", + "@octokit/rest": "^20.1.0", "@opensearch-project/opensearch": "^2.6.0", "@spotlightjs/spotlight": "^1.2.16", "@stefanprobst/rehype-extract-toc": "^2.2.0", "@t3-oss/env-nextjs": "^0.9.2", "gray-matter": "^4.0.3", + "ioredis": "^5.3.2", "langchain": "^0.1.31", + "mime-types": "^2.1.35", "next": "^14.1.4", "next-intl": "^3.10.0", "next-sitemap": "^4.2.3", @@ -92,6 +96,7 @@ "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.2", "@types/jest": "^29.5.12", + "@types/mime-types": "^2.1.4", "@types/node": "^20.12.3", "@types/react": "^18.2.74", "@typescript-eslint/eslint-plugin": "^7.5.0", diff --git a/site.config.ts b/site.config.ts index 3d3c3cd3..b4673d13 100644 --- a/site.config.ts +++ b/site.config.ts @@ -2,7 +2,7 @@ interface Menu { component: string; collection: string | null; } -interface ContentItem { +export interface ContentItem { source: string; repo: string; owner: string; diff --git a/src/app/api/content/github/route.ts b/src/app/api/content/github/route.ts new file mode 100644 index 00000000..463e287d --- /dev/null +++ b/src/app/api/content/github/route.ts @@ -0,0 +1,112 @@ +import mime from 'mime-types'; +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; +import path from 'path'; +import * as util from 'util'; + +import { commitFileToBranch, getAllFiles, getFileContent } from '@/lib/Github'; +import { logger } from '@/lib/Logger'; + +export const config = { + api: { + responseLimit: '8mb', + }, +}; + +export async function GET(req: NextRequest) { + logger.info( + `[GET /api/content/github][query]: ${util.inspect(Object.fromEntries(req.nextUrl.searchParams))}`, + ); + try { + const { + owner, + repo, + branch = 'main', + path: filepath, + } = Object.fromEntries(req.nextUrl.searchParams); + logger.info( + `[GET /api/content/github][query]: branch:${branch}, path:${filepath}, owner:${owner}, repo:${repo}`, + ); + if (!owner || !repo || typeof filepath !== 'string') { + return NextResponse.json( + { error: 'Missing required parameters: owner, repo, or path' }, + { status: 400 }, + ); + } + + if (filepath.endsWith('/')) { + // Remove trailing slash + const trimmedPath = filepath.slice(0, -1); + const files = await getAllFiles(owner, repo, branch, trimmedPath); + + return NextResponse.json({ files }); + } + const data = await getFileContent(owner, repo, branch, filepath); + const extension = path.extname(filepath); + const contentType = mime.lookup(extension) || 'application/octet-stream'; + logger.info(`[GET /api/content/github][data]: ${util.inspect(data)}`); + + const headers = new Headers(); + headers.set('Content-Type', contentType); + if (data?.content) { + return new NextResponse( + data?.content, + // Buffer.from(data?.content.toString(), data.encoding as BufferEncoding), + { status: 200, statusText: 'OK', headers }, + ); + } + return NextResponse.json( + { error: `File not found: ${filepath}` }, + { status: 404 }, + ); + } catch (err) { + return NextResponse.json( + { error: `Error in API: ${err}` }, + { status: 500 }, + ); + } +} + +export async function POST(req: NextRequest): Promise { + try { + const { + owner, + repo, + branch = 'main', + path: filepath, + } = Object.fromEntries(req.nextUrl.searchParams); + + if (!owner || !repo || typeof filepath !== 'string') { + return NextResponse.json( + { error: 'Missing required parameters: owner, repo, or path' }, + { status: 400 }, + ); + } + + const { content, message } = JSON.parse( + req?.body ? req.body.toString() : '', + ); + if (!content || !message) { + return NextResponse.json( + { + error: 'Missing required parameters: content or message in the body', + }, + { status: 400 }, + ); + } + const commitResponse = await commitFileToBranch( + owner, + repo, + branch, + filepath, + content, + message, + ); + NextResponse.json({ response: commitResponse }, { status: 201 }); + } catch (err) { + return NextResponse.json( + { error: `Error in API: ${err}` }, + { status: 500 }, + ); + } +} diff --git a/src/app/docs/[[...path]]/index.jsx.old b/src/app/docs/[[...path]]/index.jsx.old new file mode 100644 index 00000000..43f30853 --- /dev/null +++ b/src/app/docs/[[...path]]/index.jsx.old @@ -0,0 +1,161 @@ +import React from "react"; +import { siteConfig } from "../../site.config.js"; +import { parse } from "toml"; +import { getAllFiles, getFileContent } from "@/lib/github"; +import { + usePageContent, + collectionName, + usePageMenu, + LeftMenuFunction, + LeftMenu, + LeftMenuOpen, +} from "@/lib/hooks"; +import { getMenuStructure, groupMenu, getFrontMatter } from "@/lib/content"; + +import { ContentPage, IndexView } from "@/components/layouts"; +import { HeaderMinimalMenu } from "@/components/menus"; +// import { ServicesHeader } from "@/components/headers"; +import LandingPage from "@/components/landingpage"; +import { dirname } from "path"; + +export default function Page({ + type, + content: initialContent, + context: initialContext, + tiles, + loading, +}) { + if (type === "home") { + return ; + } else if (type === "404") { + return ; + } else if (type === "index") { + if (loading) { + return ( + + ); + } + const { menuStructure } = usePageMenu(initialContext); + + return ( + + ); + } else { + const { + pageContent, + contentSource, + menuStructure, + handleContentChange, + handlePageReset, + context, + content, + editMode, + } = usePageContent(initialContent, initialContext); + + console.debug("[[...path]]/index:editMode: ", editMode); + + return ( + + ); + } +} + +export async function getServerSideProps(context) { + let file; + let type = ""; + let tiles = []; + // console.log("/[...path]]:getServerSideProps:context: ", context); + if (context.params.path && siteConfig.content[context.params.path[0]]) { + file = context.params.path.join("/"); + let pageContent = ""; + let pageContentText; + switch (context.params.path.length) { + case 1: + type = "index"; + file = context.params.path.join("/"); + const allTiles = await getFrontMatter( + siteConfig.content[context.params.path[0]] + ); + console.log("allTiles: ", allTiles); + tiles = allTiles.filter((tile) => { + const parts = tile.file.split("/"); // Split the file path by '/' + const fileName = parts[parts.length - 1]; // Get the last part (file name) + // Check if the path has exactly 3 parts and the file name is 'index.md' or 'index.mdx' + return ( + parts.length === 3 && + (fileName === "_index.md" || fileName === "_index.mdx") + ); + }); + break; + default: + if (!file.endsWith(".etherpad")) { + pageContent = await getFileContent( + siteConfig.content[context.params.path[0]].owner, + siteConfig.content[context.params.path[0]].repo, + siteConfig.content[context.params.path[0]].branch, + file + ); + } + + pageContentText = pageContent + ? Buffer.from(pageContent).toString("utf-8") + : ""; + type = "content"; + file = context.params.path.join("/"); + break; + } + + return { + props: { + type: type, + content: pageContentText || "", + context: { + file: file, + ...collectionName(file, siteConfig.content[context.params.path[0]]), + }, + tiles: tiles || null, + key: context.params.path, + }, + }; + } else if (context.resolvedUrl === "/") { + type = "home"; + + return { + props: { + type: type, + content: null, + context: null, + key: "home", + }, + }; + } else { + type = "404"; + return { notFound: true }; + } +} diff --git a/src/app/docs/[[...path]]/page.tsx b/src/app/docs/[[...path]]/page.tsx new file mode 100644 index 00000000..d567fc64 --- /dev/null +++ b/src/app/docs/[[...path]]/page.tsx @@ -0,0 +1,154 @@ +import type { Metadata } from "next"; +import React from "react"; +import { LandingPage } from "@/components/Layouts"; +import { siteConfig } from "../../../../site.config"; +import { notFound } from "next/navigation"; +import { getFileContent } from "@/lib/Github"; + +export const metadata: Metadata = { + title: "Airview", + description: "Airview AI", +}; + +export default async function Page({ + params, +}: { + params: { path?: string[] }; +}) { + if ( + params.path && + params.path[0] && + siteConfig.content[params.path[0] as keyof typeof siteConfig.content] + ) { + const file = params.path.join("/") as string; + let pageContent; + let pageContentText; + switch (params.path.length) { + case 0: + notFound(); + case 1: + // index page + // const allTiles = await getFrontMatter( + // siteConfig.content[params.path[0]] + // ); + // const tiles = allTiles.filter((tile) => { + // const parts = tile.file.split("/"); // Split the file path by '/' + // const fileName = parts[parts.length - 1]; // Get the last part (file name) + // // Check if the path has exactly 3 parts and the file name is 'index.md' or 'index.mdx' + // return ( + // parts.length === 3 && + // (fileName === "_index.md" || fileName === "_index.mdx") + // ); + // }); + break; + default: + // content page + if (file.endsWith(".md") || file.endsWith(".mdx")) { + const contentKey = params.path[0] as keyof typeof siteConfig.content; + const contentConfig = siteConfig?.content?.[contentKey]; + + if (contentConfig?.owner && contentConfig?.repo && contentConfig?.branch && file) { + const { owner, repo, branch } = contentConfig; + pageContent = await getFileContent(owner, repo, branch, file); + } else { + notFound(); + } + } + console.log("pageContent: ", pageContent); + pageContentText = pageContent?.content + ? Buffer.from(pageContent.content).toString() + : ""; + break; + } + + return ( +
+

{pageContentText}

+
+ ); + } else if (params.path && params.path[0] === "home") { + // home page + + return ( +
+ +
+ ); + } else { + notFound(); + } +} + +// export async function getServerSideProps(context) { +// let file; +// let type = ""; +// let tiles = []; +// // console.log("/[...path]]:getServerSideProps:context: ", context); +// if (context.params.path && siteConfig.content[context.params.path[0]]) { +// file = context.params.path.join("/"); +// let pageContent = ""; +// let pageContentText; +// switch (context.params.path.length) { +// case 1: +// type = "index"; +// file = context.params.path.join("/"); +// const allTiles = await getFrontMatter( +// siteConfig.content[context.params.path[0]] +// ); +// console.log("allTiles: ", allTiles); +// tiles = allTiles.filter((tile) => { +// const parts = tile.file.split("/"); // Split the file path by '/' +// const fileName = parts[parts.length - 1]; // Get the last part (file name) +// // Check if the path has exactly 3 parts and the file name is 'index.md' or 'index.mdx' +// return ( +// parts.length === 3 && +// (fileName === "_index.md" || fileName === "_index.mdx") +// ); +// }); +// break; +// default: +// if (!file.endsWith(".etherpad")) { +// pageContent = await getFileContent( +// siteConfig.content[context.params.path[0]].owner, +// siteConfig.content[context.params.path[0]].repo, +// siteConfig.content[context.params.path[0]].branch, +// file +// ); +// } + +// pageContentText = pageContent +// ? Buffer.from(pageContent).toString("utf-8") +// : ""; +// type = "content"; +// file = context.params.path.join("/"); +// break; +// } + +// return { +// props: { +// type: type, +// content: pageContentText || "", +// context: { +// file: file, +// ...collectionName(file, siteConfig.content[context.params.path[0]]), +// }, +// tiles: tiles || null, +// key: context.params.path, +// }, +// }; +// } else if (context.resolvedUrl === "/") { +// type = "home"; + +// return { +// props: { +// type: type, +// content: null, +// context: null, +// key: "home", +// }, +// }; +// } else { +// type = "404"; +// return { notFound: true }; +// } +// } diff --git a/src/app/docs/page.tsx b/src/app/docs/page.tsx deleted file mode 100644 index 16b47eba..00000000 --- a/src/app/docs/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Metadata } from 'next'; -import React from 'react'; -import { LandingPage } from '@/components/Layouts'; - -export const metadata: Metadata = { - title: 'Airview', - description: 'Airview AI', -}; - -export default function Home() { - // const posts = getAllPosts(["title", "date", "excerpt", "coverImage", "slug"]); - - return ( -
- -
- ); -} diff --git a/src/lib/Github.ts b/src/lib/Github.ts new file mode 100644 index 00000000..4b082713 --- /dev/null +++ b/src/lib/Github.ts @@ -0,0 +1,814 @@ +import crypto from 'node:crypto'; + +import { createAppAuth } from '@octokit/auth-app'; +import { Octokit } from '@octokit/rest'; +// import axios from 'axios'; +import fs from 'fs'; +import * as util from 'util'; + +import { logger } from '@/lib/Logger'; +import { cacheRead, cacheWrite } from '@/lib/Redis'; + +let gitHubInstance: Octokit | undefined; + +interface GitHubConfig { + privateKey: string; + appId: string; + installationId: string; +} +const getGitHubConfiguration = (): GitHubConfig => { + let privateKey = process.env.GITHUB_PRIVATE_KEY!; + if (!privateKey) { + const privateKeyPath = process.env.GITHUB_PRIVATE_KEY_FILE!; + privateKey = fs.readFileSync(privateKeyPath, 'utf-8'); + privateKey = crypto + .createPrivateKey(privateKey) + .export({ + type: 'pkcs8', + format: 'pem', + }) + .toString(); + } + return { + privateKey, + appId: process.env.GITHUB_APP_ID!, + installationId: process.env.GITHUB_INSTALLATION_ID!, + }; +}; + +// function getGitHubConfiguration() { +// let privateKey = process.env.GITHUB_PRIVATE_KEY; +// if (!privateKey) { +// const privateKeyPath = process.env.GITHUB_PRIVATE_KEY_FILE; +// if (!privateKeyPath) { +// throw new Error('Private key file path is not defined.'); +// } +// privateKey = fs.readFileSync(privateKeyPath, 'utf-8'); +// privateKey = crypto +// .createPrivateKey(privateKey) +// .export({ +// type: 'pkcs8', +// format: 'pem', +// }) +// .toString(); +// } +// return { +// privateKey, +// appId: process.env.GITHUB_APP_ID, +// installationId: process.env.GITHUB_INSTALLATION_ID, +// }; +// } +export const createGitHubInstance = ( + config = getGitHubConfiguration(), +): Octokit => { + const octokit = new Octokit({ + authStrategy: createAppAuth, + auth: { + appId: config.appId, + privateKey: config.privateKey, + installationId: config.installationId, + }, + }); + return octokit; +}; + +// export function createGitHubInstance(config = getGitHubConfiguration()) { +// try { +// const octokit = new Octokit({ +// authStrategy: createAppAuth, +// auth: { +// appId: config.appId, +// privateKey: config.privateKey, +// installationId: config.installationId, +// }, +// }); +// return octokit; +// } catch (e) { +// throw new Error( +// `[GitHub] Could not create a GitHub instance: ${(e as Error).message}`, +// ); +// } +// } + +// // Function to get a file content +// export async function getBranchSha( +// owner: string, +// repo: string, +// branch: string, +// ): Promise { +// if (!gitHubInstance) { +// gitHubInstance = await createGitHubInstance(); +// } +// try { +// // Generate a unique cache key for this file +// const cacheKey = `github:getBranch:${owner}:${repo}:${branch}`; + +// // Check if the content is in the cache +// const cachedContent = await cacheRead(cacheKey); +// if (cachedContent) { +// // logger.info('[Github][Cache][HIT]:',cacheKey ) +// // If the content was found in the cache, return it +// return cachedContent; +// } +// logger.info('[Github][getBranchSha][Cache][MISS]:', cacheKey); + +// const branchSha = await gitHubInstance.rest.repos.getBranch({ +// owner, +// repo, +// branch, +// }); + +// try { +// // Store the content in the cache before returning it +// await cacheWrite(cacheKey, branchSha.data.commit.sha, 600); +// } catch (error) { +// logger.error(`[GitHub][getBranchSha] Error writing cache: ${error}`); +// } +// return branchSha.data.commit.sha; +// } catch (error) { +// logger.error(`[GitHub][getBranchSha] Error getting sha: ${error}`); +// // throw new Error(`[GitHub][getBranchSha] Could not get sha for branch`); +// } +// return ''; +// } + +export const getBranchSha = async ( + owner: string, + repo: string, + branch: string, +): Promise => { + if (!gitHubInstance) { + gitHubInstance = createGitHubInstance(); + } + + const cacheKey = `github:getBranch:${owner}:${repo}:${branch}`; + const cachedContent = await cacheRead(cacheKey); + if (cachedContent) return cachedContent; + + const { data } = await gitHubInstance.rest.repos.getBranch({ + owner, + repo, + branch, + }); + + await cacheWrite(cacheKey, data.commit.sha, 600); + return data.commit.sha; +}; + +type CachedContent = { + ref: string | undefined; + contributors: { authorName: string; authorDate: string }[]; + encoding: string | undefined; + content: { + data: Buffer | string; + type: string; + }; +}; + +async function getCachedFileContent( + owner: string, + repo: string, + branch: string, + path: string, + sha: string | undefined = undefined, +): Promise<{ + content: Buffer | undefined; + encoding: string; + contributors: { authorName: string; authorDate: string }[]; +} | null> { + // Generate a unique cache key for this file + const branchSha = await getBranchSha(owner, repo, branch); + let cacheKey = ''; + if (sha) { + cacheKey = `github:content:${owner}:${repo}:${sha}:${path}`; + const cachedContent: CachedContent = await cacheRead(cacheKey); + if (cachedContent) { + logger.info(`[Github][getCachedFileContent][HIT]: ${cacheKey}`); + if (cachedContent && cachedContent.encoding) { + if (cachedContent.encoding !== 'none') { + return { + content: Buffer.from( + cachedContent.content.data as string, + cachedContent.encoding as BufferEncoding, + ), + encoding: cachedContent.encoding as string, + contributors: cachedContent.contributors, + }; + // return cachedContent.content.data.toString( + // cachedContent.encoding as BufferEncoding, + // ); + } + // if (cachedContent.content.type === 'Buffer') { + // return Buffer.from(cachedContent.content.data.toString(), 'binary'); + // } + } + // return cachedContent.content.data; + } + return null; + } + cacheKey = `github:ref:${owner}:${repo}:${branchSha}:${path}`; + const cachedContent: CachedContent = await cacheRead(cacheKey); + + if (cachedContent) { + try { + const ref = JSON.parse(cachedContent.toString()); + logger.info(`[Github][getCachedFileContent][CacheKey]: ${cacheKey}`); + if (ref && ref.ref) { + logger.info( + `[Github][getCachedFileContent][Ref]: github:getContent:${owner}:${repo}:${ref.ref}:${path}`, + ); + const cachedRefContent = await cacheRead( + `github:content:${owner}:${repo}:${ref.ref}:${path}`, + ); + if (cachedRefContent && cachedRefContent.encoding) { + logger.info( + `[Github][getCachedFileContent][HIT/cachedRefContent]: ${util.inspect(cachedRefContent)}`, + ); + logger.info(`[Github][Read][HIT/Sha]: ${cacheKey} ref: ${ref.ref}`); + // if (cachedRefContent.content.type === 'Buffer') { + // return Buffer.from(cachedRefContent.content.data, 'utf-8'); + // } + if (cachedRefContent.encoding !== 'none') { + return cachedRefContent.content.data.toString( + cachedRefContent.encoding, + ); + } + // return cachedRefContent.content; + } + logger.info('[Github][Read][MISS/Sha]:', cacheKey, ' ref:', ref.ref); + } else { + // logger.info('[Github][Read][HIT/Branch]:', cacheKey) + // return cachedContent.content.data; + } + } catch (error) { + logger.info(`[Github][Read/Ref][Error]: ${cacheKey} error: ${error}`); + return null; + } + } else { + logger.info(`[Github][Read][MISS/All]: ${cacheKey}`); + return null; + } + + logger.info(`[Github][Read][MISS]: ${cacheKey}`); + return null; +} + +async function getGitHubFileContent( + owner: string, + repo: string, + branch: string, + path: string, +): Promise<{ + content: Buffer | undefined; + encoding: string; + contributors: { authorName: string; authorDate: string }[]; +} | null> { + if (!gitHubInstance) { + gitHubInstance = await createGitHubInstance(); + } + const branchSha = await getBranchSha(owner, repo, branch); + + try { + const response = (await gitHubInstance.repos.getContent({ + owner, + repo, + path, + ref: branchSha, + })) as { + data: { + encoding: string; + sha: string; + content: string; + download_url: string; + }; + }; + + const { data: commits } = await gitHubInstance.repos.listCommits({ + owner, + repo, + path, + }); + + const contributors = commits.reduce( + (acc: { authorName: string; authorDate: string }[], commit) => { + const authorName = commit.commit.author?.name ?? ''; + const authorDate = new Date( + commit.commit.author?.date ?? '', + ).toDateString(); + + const pair: { authorName: string; authorDate: string } = { + authorName, + authorDate, + }; + + const index = acc.findIndex( + (item: { authorName: string; authorDate: string }) => + item.authorName === pair.authorName && + item.authorDate === pair.authorDate, + ); + + if (index === -1) { + acc.push(pair); + } + + return acc; + }, + [], + ); + + const { encoding, sha } = response.data; + logger.info(`github:getContent:response ${util.inspect(response)}`); + + let content; + if (encoding === 'base64') { + // Decode base64 content for image files + content = Buffer.from(response.data.content, 'base64'); + } else if (encoding === 'utf-8') { + // For text files, assume UTF-8 encoding + content = Buffer.from(response.data.content, 'utf-8'); + } else if (encoding === 'none') { + // large URL. get direct + logger.info( + `github:getContent:download_url ${response.data.download_url}`, + ); + const downloadResponse = await fetch(response.data.download_url); + const downloadBuffer = await downloadResponse.arrayBuffer(); + + content = Buffer.from(downloadBuffer); + // logger.info('github:getContent:downloadResponse ', downloadResponse.data.split('\n').slice(0, 10).join('\n')); + } + try { + if (sha) { + // Store a link from the branchSha to the file + const ref = { ref: sha }; + await cacheWrite( + `github:ref:${owner}:${repo}:${branchSha}:${path}`, + JSON.stringify(ref), + ); // cache perpetually a reference to the file + const isBuffer = Buffer.isBuffer(content); + const stringifiedValue = isBuffer + ? JSON.stringify({ buffer: Array.from(content || []) }) + : JSON.stringify(content); + + await cacheWrite( + `github:content:${owner}:${repo}:${sha}:${path}`, + JSON.stringify({ + content: stringifiedValue, + encoding, + contributors, + }), + ); // cache perpetually the file contents + // logger.debug( + // `[GitHub][Write][CachedFileAndRef] : ${path} : encoding: ${response.data.encoding}`, + // ); + logger.info( + `[GitHub][getGitHubFileContent][CachedFileAndRef][Ref] : github:getContent:${owner}:${repo}:${branchSha}:${path}`, + ); + // logger.debug( + // `[GitHub][Write][CachedFileAndRef][Content] : github:getContent:${owner}:${repo}:${response.data.sha}:${path}`, + // ); + } else { + // Store the content in the cache before returning it + // await cacheWrite(cacheKey, { content, encoding, contributors }); // cache for 24 hours + // logger.debug(`[GitHub][Write][Cache] : ${path}`); + } + } catch (error) { + logger.error(`[GitHub][Write] Error writing cache: ${error}`); + } + return { content, encoding, contributors }; + } catch (error) { + logger.error( + `[GitHub][getFileContent] Error retrieving file (${path}) content: ${error}`, + ); + // throw new Error(`[GitHub][getFileContent] Could not get file`); + // logger.error('Error retrieving file content:', error, 'path:', path); + return null; + } +} +// Function to get a file content +export async function getFileContent( + owner: string, + repo: string, + branch: string, + path: string, + sha: string | undefined = undefined, +): Promise<{ + content: Buffer | undefined; + encoding: string; + contributors: { + authorName: string; + authorDate: string; + }[]; +} | null> { + // if the SHA is passed, this is a specific revision of a file. + // if not, pull back the generic revision of the file, stored with the branch sha instead. + + const cachedContent = await getCachedFileContent( + owner, + repo, + branch, + path, + sha, + ); + logger.info(`github:getFileContent:getCachedFileContent ${cachedContent}`); + if (cachedContent) { + logger.info(`[GitHub][getGitHubFileContent][Cache/Hit]: ${path}`); + return cachedContent; + } + const file = await getGitHubFileContent(owner, repo, branch, path); + + const unencodedContent = Buffer.from( + file?.content?.toString() || '', + 'binary', + ); + + logger.info(`[GitHub][getGitHubFileContent][content]: ${util.inspect(file)}`); + logger.info( + `[GitHub][getGitHubFileContent][unencodedContent]: ${unencodedContent}`, + ); + + return file; +} + +function createFilterRegex(filter: string) { + const escapedFilter = filter.replace(/\./g, '\\.').replace(/\*/g, '.*'); + return new RegExp(`^.*${escapedFilter}$`, 'i'); +} + +async function getAllFilesRecursive( + owner: string, + repo: string, + sha: string, + path: string, + recursive: boolean = true, + filter: string | undefined = undefined, +) { + if (!gitHubInstance) { + gitHubInstance = await createGitHubInstance(); + } + const response = await gitHubInstance.repos.getContent({ + owner, + repo, + path, + ref: sha, + }); + const fileObjects = ( + response.data as { type: 'file' | 'dir'; path: string; sha: string }[] + ).filter((obj) => obj.type === 'file'); + let files = fileObjects.map((obj) => ({ path: obj.path, sha: obj.sha })); + // logger.info('files: ', files) + if (recursive) { + const dirObjects = ( + response.data as { + type: 'dir' | 'file' | 'submodule' | 'symlink'; + size: number; + name: string; + path: string; + content?: string | undefined; + sha: string; + url: string; + git_url: string | null; + html_url: string | null; + download_url: string | null; + }[] + ).filter((obj) => obj.type === 'dir'); + const subPromises = dirObjects.map(async (dirObject) => { + const subPath = path ? `${path}/${dirObject.name}` : dirObject.name; + return getAllFilesRecursive(owner, repo, sha, subPath, recursive, filter); + }); + const subFiles = await Promise.all(subPromises); + files = files.concat(...subFiles); + } + if (filter) { + const regex = createFilterRegex(filter); + files = files.filter((file) => regex.test(file.path)); + } + + return files; +} + +// Function to get all files for a given path +export async function getAllFiles( + owner: string, + repo: string, + branch: string, + path: string, + recursive: boolean = true, + filter: string | undefined = undefined, +) { + if (!gitHubInstance) { + gitHubInstance = await createGitHubInstance(); + } + const branchSha = await getBranchSha(owner, repo, branch); + const files = await getAllFilesRecursive( + owner, + repo, + branchSha, + path, + recursive, + filter, + ); + return files; +} + +// const linkParser = (linkHeader: string): string | null => { +// const re = /<.*(?=>; rel=\"next\")/g; +// let arrRes: RegExpExecArray | null = []; +// while ((arrRes = re.exec(linkHeader)) !== null) { +// return arrRes[0].split('<').slice(-1)[0]; +// } +// return null; +// }; + +// const getData = async (url) => { +// const resp = await fetch(url, { +// headers: { +// // Add any necessary headers here +// }, +// }); +// if (resp.status !== 200) { +// throw Error( +// `Bad status getting branches ${resp.status} ${await resp.text()}`, +// ); +// } +// const data = await resp.json(); +// const mapped = data.map((item) => ({ +// name: item.name, +// sha: item.commit.sha, +// isProtected: item.protected, +// })); + +// const next = linkParser(resp.headers.get('Link')); +// return { mapped, next }; +// }; + +// export const getBranches = async () => { +// let link = `${GITHUB_REPO_URI}/branches?per_page=100`; +// let final = []; + +// while (link) { +// const { mapped, next } = await getData(link); +// link = next; +// final = final.concat(mapped); +// } +// return final; +// }; + +export async function getBranches( + owner: string, + repo: string, +): Promise<{ name: string; commit: { sha: string }; protected: boolean }[]> { + if (!gitHubInstance) { + gitHubInstance = await createGitHubInstance(); + } + try { + // Generate a unique cache key for this file + const cacheKey = `github:getBranches:${owner}:${repo}`; + + // Check if the content is in the cache + const cachedContent = await cacheRead(cacheKey); + if (cachedContent) { + logger.info('[Github][getBranches][HIT]:', cacheKey); + // If the content was found in the cache, return it + // return cachedContent; + } else { + logger.info('[Github][getBranches][Cache][MISS]:', cacheKey); + } + + // Fetch branches + const branches = await gitHubInstance.paginate( + gitHubInstance.repos.listBranches, + { + owner, + repo, + per_page: 100, + }, + ); + + // Filter branches with protected set to false + // const unprotectedBranches = branches.filter((branch) => !branch.protected); + const unprotectedBranches = branches; + + try { + // Store the content in the cache before returning it + await cacheWrite(cacheKey, JSON.stringify(unprotectedBranches), 600); + } catch (error) { + logger.error(`[GitHub][getBranches] Error writing cache: ${error}`); + } + return unprotectedBranches; + } catch (error) { + logger.error(`[GitHub][getBranches] Error getting sha: ${error}`); + // throw new Error(`[GitHub][getBranchSha] Could not get sha for branch`); + } + return []; +} + +// Function to create a new branch +export async function createBranch( + owner: string, + repo: string, + branch: string, + sourceBranch: string, +) { + if (!gitHubInstance) { + gitHubInstance = await createGitHubInstance(); + } + + try { + const sourceBranchSha = await getBranchSha(owner, repo, sourceBranch); + + const response = await gitHubInstance.rest.git.createRef({ + owner, + repo, + ref: `refs/heads/${branch}`, + sha: sourceBranchSha, + }); + + return response.data; + } catch (error) { + logger.error(`[GitHub][createBranch] Error creating branch: ${error}`); + throw new Error(`Could not create branch: ${error}`); + } +} + +function isBase64(str: string) { + try { + return btoa(atob(str)) === str; + } catch (err) { + return false; + } +} + +// Function to commit a file to a branch +export async function commitFileToBranch( + owner: string, + repo: string, + branch: string, + path: string, + content: string, + message: string, +) { + if (!gitHubInstance) { + gitHubInstance = await createGitHubInstance(); + } + + try { + const branchSha = await getBranchSha(owner, repo, branch); + const encoding = isBase64(content) ? 'base64' : 'utf-8'; + + const blob = await gitHubInstance.rest.git.createBlob({ + owner, + repo, + content, + encoding, + }); + + const tree = await gitHubInstance.rest.git.createTree({ + owner, + repo, + base_tree: branchSha, + tree: [ + { + path, + mode: '100644', + type: 'blob', + sha: blob.data.sha, + }, + ], + }); + + const newCommit = await gitHubInstance.rest.git.createCommit({ + owner, + repo, + message, + tree: tree.data.sha, + parents: [branchSha], + }); + + await gitHubInstance.rest.git.updateRef({ + owner, + repo, + ref: `heads/${branch}`, + sha: newCommit.data.sha, + }); + + // refresh branch cache + try { + // Store the content in the cache before returning it + const cacheKey = `github:getBranch:${owner}:${repo}:${branch}`; + await cacheWrite(cacheKey, newCommit.data.sha, 600); + } catch (error) { + logger.error(`[GitHub][getBranchSha] Error writing cache: ${error}`); + } + + return newCommit.data; + } catch (error) { + logger.error( + `[GitHub][commitFileToBranch] Error committing file: ${error}`, + ); + throw new Error(`${error}`); + } +} + +export async function commitFileChanges( + owner: string, + repo: string, + branch: string, + path: string, + content: string, + message: string, +) { + // use in pages + try { + const response = await fetch( + `/api/content/github/${owner}/${repo}?branch=${branch}&path=${path}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ content, message }), + }, + ); + + if (!response.ok) { + const data = await response.json(); + + logger.info('/lib/github/commitFileChanges:response: ', data); + if (data.error) { + throw new Error(`Error committing file: ${data.error}`); + } else { + throw new Error(`Error committing file: ${response.status}`); + } + } + + const data = await response.json(); + logger.info('Commit successful:', data); + // // refresh branch cache + // const newBranchSha = await gitHubInstance.rest.repos.getBranch({ + // owner, + // repo, + // branch, + // }); + + // try { + // // Store the content in the cache before returning it + // const cacheKey = `github:getBranch:${owner}:${repo}:${branch}`; + // await cacheWrite(cacheKey, newBranchSha.data.commit.sha, 600); + // } catch (error) { + // logger.error(`[GitHub][getBranchSha] Error writing cache: ${error}`); + // } + } catch (e) { + logger.error(`Error committing file: ${e}`); + throw new Error(`${e}`); + } +} + +export const createPR = async ( + owner: string, + repo: string, + title: string, + body: string, + head: string, + base: string, +) => { + if (!gitHubInstance) { + gitHubInstance = await createGitHubInstance(); + } + try { + const response = await gitHubInstance.rest.pulls.create({ + owner, + repo, + title, + body, + head, + base, + }); + + return response.data; + } catch (error) { + throw new Error(String(error)); + } +}; +// export const raisePR = async (owner, repo, title, body, head, base) => { +// try { +// const response = await fetch('/api/repo/pr', { +// method: 'POST', +// headers: { +// 'Content-Type': 'application/json', +// }, +// body: JSON.stringify({ owner, repo, title, body, head, base }), +// }); + +// const data = await response.json(); +// // logger.info('lib/github/raisePR:response: ', data) +// if (!response.ok) { +// throw Error(data.error || 'Network response was not ok'); +// } + +// return data; +// } catch (error) { +// // logger.error('There has been a problem with your fetch operation:', error); +// throw Error(error.message); +// } +// }; diff --git a/src/lib/Redis.ts b/src/lib/Redis.ts new file mode 100644 index 00000000..17a21861 --- /dev/null +++ b/src/lib/Redis.ts @@ -0,0 +1,231 @@ +import { logger } from '@/lib/Logger'; + +const Redis = require('ioredis'); + +let redisInstance: typeof Redis; + +function getRedisConfiguration() { + return { + host: process.env.REDIS_HOST || '172.17.0.1', + password: process.env.REDIS_PASSWORD, + port: process.env.REDIS_PORT, + }; +} + +export async function createRedisInstance(config = getRedisConfiguration()) { + try { + const options = { + enableReadyCheck: true, + scaleReads: 'all', + redisOptions: { + host: config.host, + lazyConnect: true, + showFriendlyErrorStack: true, + enableAutoPipelining: true, + maxRetriesPerRequest: 0, + retryStrategy: (times: number) => { + if (times > 3) { + throw new Error( + `[Redis] Could not connect after ${times} attempts`, + ); + } + + return Math.min(times * 200, 1000); + }, + }, + }; + + // if (config.port) { + // options.redisOptions.port = config.port; + // } + + // if (config.password) { + // options.redisOptions.password = config.password; + // } + + let redis; + + if (process.env.REDIS_CLUSTER_MODE) { + redis = new Redis.Cluster( + [ + { + host: config.host, + port: config.port, + }, + ], + options, + ); + } else { + // For local development or non-clustered environment + redis = new Redis(options.redisOptions); + } + + return redis; + } catch (e) { + throw new Error(`[Redis] Could not create a Redis instance`); + } +} + +// Function to read data from the cache +export async function cacheRead(key: string) { + try { + if (!redisInstance) { + redisInstance = await createRedisInstance(); + } + const value = await redisInstance.get(key); + if (!value) return null; + const parsedValue = JSON.parse(value); + return parsedValue.buffer ? Buffer.from(parsedValue.buffer) : parsedValue; + } catch (error) { + // Handle the error here if Redis is unavailable or there was a connection error. + // For example, you may use a fallback cache mechanism or default values. + logger.error('Error during Redis setup:', error); + return null; + } +} + +export async function cacheMRead(keys: string[], prefix = '') { + try { + if (!redisInstance) { + redisInstance = await createRedisInstance(); + } + + // Check if keys is an array and has elements + if (!Array.isArray(keys) || keys.length === 0) return null; + + // Add prefix to each key if prefix is provided + const fullKeys = prefix ? keys.map((key) => `${prefix}${key}`) : keys; + + // Use MGET to retrieve multiple keys + const values = await redisInstance.mget(...fullKeys); + + // Process the returned values + return values.map((value: string) => { + if (value === null || value === undefined) return null; + try { + const parsedValue = JSON.parse(value); + return parsedValue.buffer + ? Buffer.from(parsedValue.buffer) + : parsedValue; + } catch (parseError) { + logger.error('Error parsing value from Redis:', parseError); + return null; + } + }); + } catch (error) { + logger.error('Error during Redis read operation:', error); + return null; + } +} + +// Function to write data to the cache +export async function cacheWrite( + key: string, + value: string, + ttl: number | undefined = undefined, +) { + try { + if (!redisInstance) { + redisInstance = await createRedisInstance(); + } + const isBuffer = Buffer.isBuffer(value); + const stringifiedValue = isBuffer + ? JSON.stringify({ buffer: Array.from(value) }) + : JSON.stringify(value); + + if (ttl) { + try { + await redisInstance.set(key, stringifiedValue, 'EX', ttl); + return true; // or return 'Data set successfully'; + } catch (error) { + logger.error(`Error setting data: ${error}`); + throw error; + } + } else { + try { + await redisInstance.set(key, stringifiedValue); + return true; // or return 'Data set successfully'; + } catch (error) { + logger.error(`Error setting data: ${error}`); + throw error; + } + } + } catch (error) { + // Handle the error here if Redis is unavailable or there was a connection error. + // For example, you may use a fallback cache mechanism or default values. + logger.error('Error during Redis setup:', error); + throw error; + } +} + +// Function to delete data from the cache +export async function cacheDelete(key: string) { + try { + if (!redisInstance) { + redisInstance = await createRedisInstance(); + } + const result = await redisInstance.del(key); + return result === 1; // Returns true if the key was deleted + } catch (error) { + // Handle the error here if Redis is unavailable or there was a connection error. + logger.error('Error during Redis deletion:', error); + throw error; + } +} + +// Function to read data from the cache +export async function cacheSearch(key: string) { + try { + if (!redisInstance) { + redisInstance = await createRedisInstance(); + } + const value = await new Promise((resolve, reject) => { + redisInstance.keys(key, (err: any, result: any) => { + if (err) { + logger.error('Error fetching keys:', err); + reject(err); + } else { + resolve(result); + } + }); + }); + + if (!value) return null; + return value; + } catch (error) { + // Handle the error here if Redis is unavailable or there was a connection error. + // For example, you may use a fallback cache mechanism or default values. + logger.error('Error during Redis setup:', error); + return []; + } +} + +export async function hset(key: any, obj: any, ttl = null) { + try { + if (!redisInstance) { + redisInstance = await createRedisInstance(); + } + if (ttl) { + try { + await redisInstance.hset(key, obj, 'EX', ttl); + return true; // or return 'Data set successfully'; + } catch (error) { + logger.error(`Error setting data: ${error}`); + throw error; + } + } else { + try { + await redisInstance.hset(key, obj); + return true; // or return 'Data set successfully'; + } catch (error) { + logger.error(`Error setting data: ${error}`); + throw error; + } + } + } catch (error) { + // Handle the error here if Redis is unavailable or there was a connection error. + // For example, you may use a fallback cache mechanism or default values. + logger.error('Error during Redis setup:', error); + return false; + } +} diff --git a/tsconfig.json b/tsconfig.json index 420e2a63..232a5d6a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -59,6 +59,6 @@ "**/*.tsx", ".storybook/*.ts", ".next/types/**/*.ts", - "src/libs/Env.mjs" + "src/lib/Env.mjs" ] }