diff --git a/package-lock.json b/package-lock.json index 4e1fb7b..2c821d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "gatsby-plugin-sitemap": "^6.14.0", "graphiql": "^3.8.3", "graphql-tag": "^2.12.6", + "jose": "^6.1.0", "lodash": "^4.17.21", "react": "^18.0.0", "react-dom": "^18.0.0", @@ -47,6 +48,7 @@ "@graphql-codegen/typescript-operations": "^4.0.0", "@tailwindcss/typography": "^0.5.0", "@types/node": "^24.7.1", + "@types/react-dom": "^19.2.3", "@types/react-syntax-highlighter": "^15.5.3", "autoprefixer": "^10.4.0", "gatsby-adapter-netlify": "^1.2.0", @@ -2515,6 +2517,33 @@ "react-dom": "^16.8.0 || ^17 || ^18" } }, + "node_modules/@graphiql/react/node_modules/framer-motion": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz", + "integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==", + "license": "MIT", + "dependencies": { + "@motionone/dom": "10.12.0", + "framesync": "6.0.1", + "hey-listen": "^1.0.8", + "popmotion": "11.0.3", + "style-value-types": "5.0.0", + "tslib": "^2.1.0" + }, + "optionalDependencies": { + "@emotion/is-prop-valid": "^0.8.2" + }, + "peerDependencies": { + "react": ">=16.8 || ^17.0.0 || ^18.0.0", + "react-dom": ">=16.8 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@graphiql/react/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@graphiql/toolkit": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.11.1.tgz", @@ -4303,6 +4332,16 @@ "ws": "*" } }, + "node_modules/@graphql-tools/prisma-loader/node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@graphql-tools/prisma-loader/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -4915,6 +4954,7 @@ "version": "10.18.0", "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.18.0.tgz", "integrity": "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==", + "license": "MIT", "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", @@ -4925,12 +4965,14 @@ "node_modules/@motionone/animation/node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/@motionone/dom": { "version": "10.12.0", "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.12.0.tgz", "integrity": "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==", + "license": "MIT", "dependencies": { "@motionone/animation": "^10.12.0", "@motionone/generators": "^10.12.0", @@ -4943,12 +4985,14 @@ "node_modules/@motionone/dom/node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/@motionone/easing": { "version": "10.18.0", "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.18.0.tgz", "integrity": "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==", + "license": "MIT", "dependencies": { "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" @@ -4957,12 +5001,14 @@ "node_modules/@motionone/easing/node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/@motionone/generators": { "version": "10.18.0", "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.18.0.tgz", "integrity": "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==", + "license": "MIT", "dependencies": { "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", @@ -4972,17 +5018,20 @@ "node_modules/@motionone/generators/node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/@motionone/types": { "version": "10.17.1", "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.17.1.tgz", - "integrity": "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==" + "integrity": "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==", + "license": "MIT" }, "node_modules/@motionone/utils": { "version": "10.18.0", "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.18.0.tgz", "integrity": "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==", + "license": "MIT", "dependencies": { "@motionone/types": "^10.17.1", "hey-listen": "^1.0.8", @@ -4992,7 +5041,8 @@ "node_modules/@motionone/utils/node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/@n1ru4l/push-pull-async-iterable-iterator": { "version": "3.2.0", @@ -8124,12 +8174,22 @@ } }, "node_modules/@types/react": { - "version": "19.0.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", - "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" } }, "node_modules/@types/react-syntax-highlighter": { @@ -12315,9 +12375,10 @@ } }, "node_modules/csstype": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", - "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==" + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" }, "node_modules/currently-unhandled": { "version": "0.4.1", @@ -15116,35 +15177,11 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/framer-motion": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz", - "integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==", - "dependencies": { - "@motionone/dom": "10.12.0", - "framesync": "6.0.1", - "hey-listen": "^1.0.8", - "popmotion": "11.0.3", - "style-value-types": "5.0.0", - "tslib": "^2.1.0" - }, - "optionalDependencies": { - "@emotion/is-prop-valid": "^0.8.2" - }, - "peerDependencies": { - "react": ">=16.8 || ^17.0.0 || ^18.0.0", - "react-dom": ">=16.8 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/framer-motion/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, "node_modules/framesync": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz", "integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==", + "license": "MIT", "dependencies": { "tslib": "^2.1.0" } @@ -15152,7 +15189,8 @@ "node_modules/framesync/node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/fresh": { "version": "0.5.2", @@ -19203,7 +19241,8 @@ "node_modules/hey-listen": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", - "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" + "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==", + "license": "MIT" }, "node_modules/highlight.js": { "version": "10.7.3", @@ -20645,10 +20684,10 @@ } }, "node_modules/jose": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.8.0.tgz", - "integrity": "sha512-E7CqYpL/t7MMnfGnK/eg416OsFCVUrU/Y3Vwe7QjKhu/BkS1Ms455+2xsqZQVN57/U2MHMBvEb5SrmAZWAIntA==", - "dev": true, + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.0.tgz", + "integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } @@ -24927,6 +24966,7 @@ "version": "11.0.3", "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz", "integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==", + "license": "MIT", "dependencies": { "framesync": "6.0.1", "hey-listen": "^1.0.8", @@ -24937,7 +24977,8 @@ "node_modules/popmotion/node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/possible-typed-array-names": { "version": "1.0.0", @@ -28614,6 +28655,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz", "integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==", + "license": "MIT", "dependencies": { "hey-listen": "^1.0.8", "tslib": "^2.1.0" @@ -28622,7 +28664,8 @@ "node_modules/style-value-types/node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/stylehacks": { "version": "5.1.0", @@ -32937,6 +32980,27 @@ "markdown-it": "^14.1.0", "react-compiler-runtime": "19.0.0-beta-37ed2a7-20241206", "set-value": "^4.1.0" + }, + "dependencies": { + "framer-motion": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz", + "integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==", + "requires": { + "@emotion/is-prop-valid": "^0.8.2", + "@motionone/dom": "10.12.0", + "framesync": "6.0.1", + "hey-listen": "^1.0.8", + "popmotion": "11.0.3", + "style-value-types": "5.0.0", + "tslib": "^2.1.0" + } + }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } } }, "@graphiql/toolkit": { @@ -34292,6 +34356,12 @@ "dev": true, "requires": {} }, + "jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "dev": true + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -36765,13 +36835,20 @@ } }, "@types/react": { - "version": "19.0.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", - "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "requires": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, + "@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "requires": {} + }, "@types/react-syntax-highlighter": { "version": "15.5.3", "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.3.tgz", @@ -39765,9 +39842,9 @@ } }, "csstype": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", - "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==" + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" }, "currently-unhandled": { "version": "0.4.1", @@ -41718,27 +41795,6 @@ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==" }, - "framer-motion": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz", - "integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==", - "requires": { - "@emotion/is-prop-valid": "^0.8.2", - "@motionone/dom": "10.12.0", - "framesync": "6.0.1", - "hey-listen": "^1.0.8", - "popmotion": "11.0.3", - "style-value-types": "5.0.0", - "tslib": "^2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - } - } - }, "framesync": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz", @@ -45510,10 +45566,9 @@ } }, "jose": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.8.0.tgz", - "integrity": "sha512-E7CqYpL/t7MMnfGnK/eg416OsFCVUrU/Y3Vwe7QjKhu/BkS1Ms455+2xsqZQVN57/U2MHMBvEb5SrmAZWAIntA==", - "dev": true + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.0.tgz", + "integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==" }, "js-tokens": { "version": "4.0.0", diff --git a/package.json b/package.json index 754e7ad..367a621 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "gatsby-plugin-sitemap": "^6.14.0", "graphiql": "^3.8.3", "graphql-tag": "^2.12.6", + "jose": "^6.1.0", "lodash": "^4.17.21", "react": "^18.0.0", "react-dom": "^18.0.0", @@ -69,6 +70,7 @@ "@graphql-codegen/typescript-operations": "^4.0.0", "@tailwindcss/typography": "^0.5.0", "@types/node": "^24.7.1", + "@types/react-dom": "^19.2.3", "@types/react-syntax-highlighter": "^15.5.3", "autoprefixer": "^10.4.0", "gatsby-adapter-netlify": "^1.2.0", diff --git a/plugins/static-redirects/gatsby-node.js b/plugins/static-redirects/gatsby-node.js index 81f09fc..5b72468 100644 --- a/plugins/static-redirects/gatsby-node.js +++ b/plugins/static-redirects/gatsby-node.js @@ -6,7 +6,10 @@ const redirects = [ exports.onPostBuild = ({ reporter, basePath, pathPrefix }) => { const redirectsPath = path.resolve(`${__dirname}/../../public/_redirects`); - const input = fs.readFileSync(redirectsPath, 'utf-8'); + let input = ""; + if (fs.existsSync(redirectsPath)) { + input = fs.readFileSync(redirectsPath, "utf-8"); + } const output = `${input}\n${redirects.join('\n')}`; console.log(output); fs.writeFileSync(redirectsPath, output); diff --git a/src/components/OidcVisualizer/OidcVisualizer.tsx b/src/components/OidcVisualizer/OidcVisualizer.tsx new file mode 100644 index 0000000..882a25c --- /dev/null +++ b/src/components/OidcVisualizer/OidcVisualizer.tsx @@ -0,0 +1,270 @@ +import React, { useEffect, useLayoutEffect, useState, useRef } from 'react'; +import { jwtVerify, createRemoteJWKSet, JWTPayload } from 'jose'; +import useLocalStorage from './hooks/useLocalStorage'; +import OidcSettingsModal from './components/OidcSettingsModal'; +import oidcConfig from './oidcConfig'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import StepOne from './components/steps/StepOne'; +import StepTwo from './components/steps/StepTwo'; +import StepThree from './components/steps/StepThree'; +import StepFour from './components/steps/StepFour'; +import { generateJWT } from './generateJwt'; +import type { OidcTokenResponse, OidcSettings } from './types'; + +const OidcVisualizer = () => { + const [authCode, setAuthCode] = useState(null); + const [tokenResponse, setTokenResponse] = useState(null); + const [decodedPayload, setDecodedPayload] = useState(null); + const [tokenRequest, setTokenRequest] = useState(null); + const [codeExchangeCompleted, setCodeExchangeCompleted] = useState(false); + const [step2Error, setStep2Error] = useState(null); + const [step3Error, setStep3Error] = useState(null); + const [showSettings, setShowSettings] = useState(false); + const [oidcSettings, setOidcSettings] = useLocalStorage( + 'oidc-settings', + oidcConfig, + ); + + const authorizeUrl = `https://${oidcSettings.domain}/oauth2/authorize?response_type=code&client_id=${oidcSettings.clientId}&redirect_uri=${encodeURIComponent(oidcConfig.redirectUri)}&scope=${encodeURIComponent(oidcSettings.scope)}${ + oidcSettings.acrValues?.length + ? `&acr_values=${encodeURIComponent(oidcSettings.acrValues.join(' '))}` + : '' + }`; + + /* Close settings modal with Escape key */ + useEffect(() => { + const handler = (event: KeyboardEvent) => { + if (event.key === 'Escape') setShowSettings(false); + }; + + document.addEventListener('keydown', handler); + return () => document.removeEventListener('keydown', handler); + }, []); + + /* Define the current step for scrolling */ + const STEP = { + STEP_1: 1, + STEP_2: 2, + STEP_3: 3, + STEP_4: 4, + } as const; + + const step1Ref = useRef(null); + const step2Ref = useRef(null); + const step3Ref = useRef(null); + const step4Ref = useRef(null); + + const currentStep = (() => { + if (decodedPayload) return STEP.STEP_4; + if (tokenResponse && codeExchangeCompleted) return STEP.STEP_3; + if (authCode) return STEP.STEP_2; + return STEP.STEP_1; + })(); + + const stepRefs = { + [STEP.STEP_1]: step1Ref, + [STEP.STEP_2]: step2Ref, + [STEP.STEP_3]: step3Ref, + [STEP.STEP_4]: step4Ref, + }; + + /* Scroll to current step on step change */ + useLayoutEffect(() => { + const el = stepRefs[currentStep].current; + if (!el) return; + + const headerHeight = 56; + const elementTop = el.getBoundingClientRect().top; + const targetY = elementTop + window.scrollY - headerHeight; + + if (tokenResponse && !decodedPayload && currentStep !== 3) { + el.scrollIntoView({ behavior: 'smooth', block: 'end' }); + // When token is received, scroll to the bottom of step #2 to show "Next" button + } else { + // Otherwise, scroll to top + window.scrollTo({ + top: targetY, + behavior: 'smooth', + }); + } + }, [currentStep, tokenResponse]); + + /* Extract authorization code from URL on mount */ + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + + if (code) { + setAuthCode(code); + window.history.replaceState({}, document.title, window.location.pathname); // Remove authorization code from URL + } + }, []); + + /* Handle code for token exchange */ + const handleExchange = async () => { + if (!authCode) return; + + try { + const params: Record = { + grant_type: 'authorization_code', + code: authCode, + redirect_uri: oidcConfig.redirectUri, + }; + + const clientAssertion = await generateJWT(oidcSettings); + + if (oidcSettings.pkJwtAuth) { + params['client_assertion_type'] = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'; + params['client_assertion'] = clientAssertion; + } else { + params['client_id'] = oidcSettings.clientId; + params['client_secret'] = oidcSettings.clientSecret; + } + + const getToken = await fetch(`https://${oidcSettings.domain}/oauth2/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams(params), + }); + + const formattedRequest = [ + `POST /oauth2/token HTTP/1.1`, + `Host: ${oidcSettings.domain}`, + `Content-Type: application/x-www-form-urlencoded`, + ``, + new URLSearchParams(params).toString().replaceAll('&', '\n'), + ].join('\n'); + + setTokenRequest(formattedRequest); + + const data: OidcTokenResponse = await getToken.json(); + + if (data.error) throw new Error(data.error_description || data.error); + setTokenResponse(data); + } catch (err: any) { + setStep2Error(err.message); + } + }; + + /* Proceed to token verification step */ + const proceedToVerifyWT = () => { + setCodeExchangeCompleted(true); + }; + + /* Handle JWT validation */ + const handleVerify = async () => { + if (!tokenResponse?.id_token) return; + + try { + const JWKS = createRemoteJWKSet( + new URL(`https://${oidcSettings.domain}/.well-known/jwks.json`), + ); + const { payload } = await jwtVerify(tokenResponse.id_token, JWKS); + setDecodedPayload(payload); + } catch (err: any) { + setStep3Error('Token Verification Failed: ' + err.message); + } + }; + + const handleReset = () => { + setAuthCode(null); + setTokenResponse(null); + setDecodedPayload(null); + setCodeExchangeCompleted(false); + setStep2Error(null); + setStep3Error(null); + window.history.replaceState({}, document.title, window.location.pathname); + }; + + /* Updating OIDC settings */ + const handleUpdateSettings = (newSettings: OidcSettings) => { + setOidcSettings(prev => ({ + ...prev, + ...newSettings, + })); + handleReset(); + setShowSettings(false); + }; + + return ( +
+
+

OpenID Connect Visualizer

+ +
+ + {/* STEP 1: Authorization */} + { + window.location.href = authorizeUrl; + }} + /> + + {/* STEP 2: Code for Token Exchange */} + + + {/* STEP 3: Token Verification */} + + + {/* STEP 4: Result */} + {decodedPayload && } + + {/* Reset Button */} + {(decodedPayload || step2Error || step3Error) && ( +
+ +
+ )} + + {/* OIDC Settings Modal */} + {showSettings && ( + setShowSettings(false)} + domain={oidcSettings.domain} + clientId={oidcSettings.clientId} + clientSecret={oidcSettings.clientSecret} + scope={oidcSettings.scope} + redirectUri={oidcConfig.redirectUri} + onSave={handleUpdateSettings} + pkJwtAuth={oidcSettings.pkJwtAuth} + acrValues={oidcSettings.acrValues} + /> + )} +
+ ); +}; + +export default OidcVisualizer; diff --git a/src/components/OidcVisualizer/components/OidcSettingsModal.tsx b/src/components/OidcVisualizer/components/OidcSettingsModal.tsx new file mode 100644 index 0000000..c8ad43b --- /dev/null +++ b/src/components/OidcVisualizer/components/OidcSettingsModal.tsx @@ -0,0 +1,273 @@ +import React, { useState } from 'react'; +import { createPortal } from 'react-dom'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { linkStyles, inputStyles, disabledInputStyles } from '../styles'; +import { PROVIDERS } from '../../../utils/auth-methods'; +import publicSigningKey from '../keys/signing_jwks_public.json'; +import oidcConfig from '../oidcConfig'; + +type ModalProps = { + open: boolean; + onClose: () => void; + onSave: (newConfig: { + domain: string; + clientId: string; + clientSecret: string; + scope: string; + pkJwtAuth: boolean; + acrValues?: string[]; + }) => void; + domain: string; + clientId: string; + clientSecret: string; + scope: string; + redirectUri: string; + pkJwtAuth: boolean; + acrValues?: string[]; +}; + +export default function Modal({ + open, + onClose, + onSave, + domain, + clientId, + clientSecret, + scope, + pkJwtAuth, + acrValues, +}: ModalProps) { + const [settings, setSettings] = useState({ + domain, + clientId, + clientSecret, + scope, + pkJwtAuth, + acrValues: acrValues || [], + }); + + const toggleAcrValue = (value: string) => { + setSettings(prev => { + const current = prev.acrValues; + const next = current.includes(value) ? current.filter(v => v !== value) : [...current, value]; + return { ...prev, acrValues: next }; + }); + }; + + const authDescriptions = { + client_secret: ( + <> + Standard client authentication using a shared{' '} + + client secret + + . + + ), + private_jwt: ( + <> + A more secure authentication option based on asymmetric cryptography. For more details, see{' '} + + Private key JWT authentication + + . + + ), + }; + + return createPortal( +
+
e.stopPropagation()} + > + + +

Client Configuration

+ +

+ This visualizer is configured to use Idura’s default application settings. You can update + them to test the OpenID Connect flow with your own Idura application instead. If you do, + make sure to add{' '} + + https://docs.idura.app/verify/guides/oidc-visualizer + {' '} + as a redirect URI in your application settings in the{' '} + + Idura dashboard + + . +

+ +
+
+ +
+ +

+ {settings.pkJwtAuth ? authDescriptions.private_jwt : authDescriptions.client_secret} +

+
+
+ {settings.pkJwtAuth && ( +
+ +
+