From 64c9b509e467996e242c8818bcc3cf6bcb39301c Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Thu, 5 Dec 2024 15:44:30 +0100 Subject: [PATCH] feat: add @ory/nextjs package (#303) --- .github/codecov.yml | 13 + .github/workflows/ci.yml | 1 + package-lock.json | 934 ++++++++++++++---- packages/nextjs/.eslintrc.json | 25 + packages/nextjs/README.md | 17 + packages/nextjs/jest.config.ts | 15 + packages/nextjs/package.json | 73 ++ packages/nextjs/project.json | 30 + packages/nextjs/src/app/client.ts | 15 + packages/nextjs/src/app/flow.test.ts | 124 +++ packages/nextjs/src/app/flow.ts | 53 + packages/nextjs/src/app/index.ts | 10 + packages/nextjs/src/app/login.ts | 53 + packages/nextjs/src/app/recovery.ts | 53 + packages/nextjs/src/app/registration.ts | 53 + packages/nextjs/src/app/utils.test.ts | 82 ++ packages/nextjs/src/app/utils.ts | 32 + packages/nextjs/src/app/verification.ts | 53 + packages/nextjs/src/index.ts | 5 + packages/nextjs/src/middleware/index.ts | 4 + .../nextjs/src/middleware/middleware.test.ts | 259 +++++ packages/nextjs/src/middleware/middleware.ts | 142 +++ packages/nextjs/src/pages/client.ts | 18 + packages/nextjs/src/pages/flow.ts | 65 ++ packages/nextjs/src/pages/index.ts | 8 + packages/nextjs/src/pages/login.ts | 22 + packages/nextjs/src/pages/recovery.ts | 16 + packages/nextjs/src/pages/registration.ts | 20 + packages/nextjs/src/pages/utils.ts | 38 + packages/nextjs/src/pages/verification.ts | 16 + packages/nextjs/src/types.ts | 88 ++ .../utils/__snapshots__/utils.test.ts.snap | 33 + packages/nextjs/src/utils/config.test.ts | 90 ++ packages/nextjs/src/utils/config.ts | 63 ++ packages/nextjs/src/utils/cookie.test.ts | 69 ++ packages/nextjs/src/utils/cookie.ts | 47 + packages/nextjs/src/utils/headers.ts | 22 + packages/nextjs/src/utils/rewrite.test.ts | 101 ++ packages/nextjs/src/utils/rewrite.ts | 77 ++ packages/nextjs/src/utils/sdk.test.ts | 107 ++ packages/nextjs/src/utils/sdk.ts | 69 ++ packages/nextjs/src/utils/utils.test.ts | 198 ++++ packages/nextjs/src/utils/utils.ts | 85 ++ packages/nextjs/tsconfig.json | 23 + packages/nextjs/tsconfig.lib.json | 11 + packages/nextjs/tsconfig.spec.json | 14 + packages/nextjs/tsup.config.ts | 58 ++ 47 files changed, 3239 insertions(+), 165 deletions(-) create mode 100644 .github/codecov.yml create mode 100644 packages/nextjs/.eslintrc.json create mode 100644 packages/nextjs/README.md create mode 100644 packages/nextjs/jest.config.ts create mode 100644 packages/nextjs/package.json create mode 100644 packages/nextjs/project.json create mode 100644 packages/nextjs/src/app/client.ts create mode 100644 packages/nextjs/src/app/flow.test.ts create mode 100644 packages/nextjs/src/app/flow.ts create mode 100644 packages/nextjs/src/app/index.ts create mode 100644 packages/nextjs/src/app/login.ts create mode 100644 packages/nextjs/src/app/recovery.ts create mode 100644 packages/nextjs/src/app/registration.ts create mode 100644 packages/nextjs/src/app/utils.test.ts create mode 100644 packages/nextjs/src/app/utils.ts create mode 100644 packages/nextjs/src/app/verification.ts create mode 100644 packages/nextjs/src/index.ts create mode 100644 packages/nextjs/src/middleware/index.ts create mode 100644 packages/nextjs/src/middleware/middleware.test.ts create mode 100644 packages/nextjs/src/middleware/middleware.ts create mode 100644 packages/nextjs/src/pages/client.ts create mode 100644 packages/nextjs/src/pages/flow.ts create mode 100644 packages/nextjs/src/pages/index.ts create mode 100644 packages/nextjs/src/pages/login.ts create mode 100644 packages/nextjs/src/pages/recovery.ts create mode 100644 packages/nextjs/src/pages/registration.ts create mode 100644 packages/nextjs/src/pages/utils.ts create mode 100644 packages/nextjs/src/pages/verification.ts create mode 100644 packages/nextjs/src/types.ts create mode 100644 packages/nextjs/src/utils/__snapshots__/utils.test.ts.snap create mode 100644 packages/nextjs/src/utils/config.test.ts create mode 100644 packages/nextjs/src/utils/config.ts create mode 100644 packages/nextjs/src/utils/cookie.test.ts create mode 100644 packages/nextjs/src/utils/cookie.ts create mode 100644 packages/nextjs/src/utils/headers.ts create mode 100644 packages/nextjs/src/utils/rewrite.test.ts create mode 100644 packages/nextjs/src/utils/rewrite.ts create mode 100644 packages/nextjs/src/utils/sdk.test.ts create mode 100644 packages/nextjs/src/utils/sdk.ts create mode 100644 packages/nextjs/src/utils/utils.test.ts create mode 100644 packages/nextjs/src/utils/utils.ts create mode 100644 packages/nextjs/tsconfig.json create mode 100644 packages/nextjs/tsconfig.lib.json create mode 100644 packages/nextjs/tsconfig.spec.json create mode 100644 packages/nextjs/tsup.config.ts diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 00000000..016d22ac --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,13 @@ +comment: + layout: "condensed_header, diff, flags, components" + +component_management: + individual_components: + - component_id: elements-react # this is an identifier that should not be changed + name: "@ory/elements-react" # this is a display name, and can be changed freely + paths: + - packages/elements-react + - component_id: nextjs # this is an identifier that should not be changed + name: "@ory/nextjs" # this is a display name, and can be changed freely + paths: + - packages/nextjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e06ec89..49598e07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,3 +51,4 @@ jobs: uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} + codecov_yml_path: .github/codecov.yml diff --git a/package-lock.json b/package-lock.json index b1bec9c3..48937074 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7243,6 +7243,10 @@ "resolved": "packages/test", "link": true }, + "node_modules/@ory/nextjs": { + "resolved": "packages/nextjs", + "link": true + }, "node_modules/@phenomnomnominal/tsquery": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@phenomnomnominal/tsquery/-/tsquery-5.0.1.tgz", @@ -7999,9 +8003,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz", - "integrity": "sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.27.4.tgz", + "integrity": "sha512-2Y3JT6f5MrQkICUyRVCw4oa0sutfAsgaSsb0Lmmy1Wi2y7X5vT9Euqw4gOsCyy0YfKURBg35nhUKZS4mDcfULw==", "cpu": [ "arm" ], @@ -8013,9 +8017,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.2.tgz", - "integrity": "sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.27.4.tgz", + "integrity": "sha512-wzKRQXISyi9UdCVRqEd0H4cMpzvHYt1f/C3CoIjES6cG++RHKhrBj2+29nPF0IB5kpy9MS71vs07fvrNGAl/iA==", "cpu": [ "arm64" ], @@ -8027,9 +8031,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.2.tgz", - "integrity": "sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.27.4.tgz", + "integrity": "sha512-PlNiRQapift4LNS8DPUHuDX/IdXiLjf8mc5vdEmUR0fF/pyy2qWwzdLjB+iZquGr8LuN4LnUoSEvKRwjSVYz3Q==", "cpu": [ "arm64" ], @@ -8041,9 +8045,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.2.tgz", - "integrity": "sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.27.4.tgz", + "integrity": "sha512-o9bH2dbdgBDJaXWJCDTNDYa171ACUdzpxSZt+u/AAeQ20Nk5x+IhA+zsGmrQtpkLiumRJEYef68gcpn2ooXhSQ==", "cpu": [ "x64" ], @@ -8054,10 +8058,38 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.27.4.tgz", + "integrity": "sha512-NBI2/i2hT9Q+HySSHTBh52da7isru4aAAo6qC3I7QFVsuhxi2gM8t/EI9EVcILiHLj1vfi+VGGPaLOUENn7pmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.27.4.tgz", + "integrity": "sha512-wYcC5ycW2zvqtDYrE7deary2P2UFmSh85PUpAx+dwTCO9uw3sgzD6Gv9n5X4vLaQKsrfTSZZ7Z7uynQozPVvWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.2.tgz", - "integrity": "sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.27.4.tgz", + "integrity": "sha512-9OwUnK/xKw6DyRlgx8UizeqRFOfi9mf5TYCw1uolDaJSbUmBxP85DE6T4ouCMoN6pXw8ZoTeZCSEfSaYo+/s1w==", "cpu": [ "arm" ], @@ -8069,9 +8101,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.2.tgz", - "integrity": "sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.27.4.tgz", + "integrity": "sha512-Vgdo4fpuphS9V24WOV+KwkCVJ72u7idTgQaBoLRD0UxBAWTF9GWurJO9YD9yh00BzbkhpeXtm6na+MvJU7Z73A==", "cpu": [ "arm" ], @@ -8083,9 +8115,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.2.tgz", - "integrity": "sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.27.4.tgz", + "integrity": "sha512-pleyNgyd1kkBkw2kOqlBx+0atfIIkkExOTiifoODo6qKDSpnc6WzUY5RhHdmTdIJXBdSnh6JknnYTtmQyobrVg==", "cpu": [ "arm64" ], @@ -8097,9 +8129,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.2.tgz", - "integrity": "sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.27.4.tgz", + "integrity": "sha512-caluiUXvUuVyCHr5DxL8ohaaFFzPGmgmMvwmqAITMpV/Q+tPoaHZ/PWa3t8B2WyoRcIIuu1hkaW5KkeTDNSnMA==", "cpu": [ "arm64" ], @@ -8111,9 +8143,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.2.tgz", - "integrity": "sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.27.4.tgz", + "integrity": "sha512-FScrpHrO60hARyHh7s1zHE97u0KlT/RECzCKAdmI+LEoC1eDh/RDji9JgFqyO+wPDb86Oa/sXkily1+oi4FzJQ==", "cpu": [ "ppc64" ], @@ -8125,9 +8157,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.2.tgz", - "integrity": "sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.27.4.tgz", + "integrity": "sha512-qyyprhyGb7+RBfMPeww9FlHwKkCXdKHeGgSqmIXw9VSUtvyFZ6WZRtnxgbuz76FK7LyoN8t/eINRbPUcvXB5fw==", "cpu": [ "riscv64" ], @@ -8139,9 +8171,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.2.tgz", - "integrity": "sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.27.4.tgz", + "integrity": "sha512-PFz+y2kb6tbh7m3A7nA9++eInGcDVZUACulf/KzDtovvdTizHpZaJty7Gp0lFwSQcrnebHOqxF1MaKZd7psVRg==", "cpu": [ "s390x" ], @@ -8153,9 +8185,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz", - "integrity": "sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.4.tgz", + "integrity": "sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q==", "cpu": [ "x64" ], @@ -8167,9 +8199,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz", - "integrity": "sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.4.tgz", + "integrity": "sha512-5AeeAF1PB9TUzD+3cROzFTnAJAcVUGLuR8ng0E0WXGkYhp6RD6L+6szYVX+64Rs0r72019KHZS1ka1q+zU/wUw==", "cpu": [ "x64" ], @@ -8181,9 +8213,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.2.tgz", - "integrity": "sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.27.4.tgz", + "integrity": "sha512-yOpVsA4K5qVwu2CaS3hHxluWIK5HQTjNV4tWjQXluMiiiu4pJj4BN98CvxohNCpcjMeTXk/ZMJBRbgRg8HBB6A==", "cpu": [ "arm64" ], @@ -8195,9 +8227,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.2.tgz", - "integrity": "sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.27.4.tgz", + "integrity": "sha512-KtwEJOaHAVJlxV92rNYiG9JQwQAdhBlrjNRp7P9L8Cb4Rer3in+0A+IPhJC9y68WAi9H0sX4AiG2NTsVlmqJeQ==", "cpu": [ "ia32" ], @@ -8209,9 +8241,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz", - "integrity": "sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==", + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.27.4.tgz", + "integrity": "sha512-3j4jx1TppORdTAoBJRd+/wJRGCPC0ETWkXOecJ6PPZLj6SptXkrXcNqdj0oclbKML6FkQltdz7bBA3rUSirZug==", "cpu": [ "x64" ], @@ -11810,6 +11842,13 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/detect-port": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/detect-port/-/detect-port-1.3.5.tgz", @@ -11850,9 +11889,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, "license": "MIT" }, @@ -12136,6 +12175,13 @@ "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", "license": "MIT" }, + "node_modules/@types/psl": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/psl/-/psl-1.1.3.tgz", + "integrity": "sha512-Iu174JHfLd7i/XkXY6VDrqSlPvTDQOtQI7wNAXKKOAADJ9TduRLkNdMgjGiMxSttUIZnomv81JAbAbC0DhggxA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/qs": { "version": "6.9.15", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", @@ -12207,6 +12253,16 @@ "@types/send": "*" } }, + "node_modules/@types/set-cookie-parser": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz", + "integrity": "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -22598,6 +22654,17 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-esm-transformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jest-esm-transformer/-/jest-esm-transformer-1.0.0.tgz", + "integrity": "sha512-FoPgeMMwy1/CEsc8tBI41i83CEO3x85RJuZi5iAMmWoARXhfgk6Jd7y+4d+z+HCkTKNVDvSWKGRhwjzU9PUbrw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@babel/core": "^7.4.4", + "@babel/plugin-transform-modules-commonjs": "^7.4.4" + } + }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -27152,17 +27219,18 @@ "license": "MIT" }, "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true, - "license": "MIT" + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.14.0.tgz", + "integrity": "sha512-Syk1bnf6fRZ9wQs03AtKJHcM12cKbOLo9L8JtCCdYj5/DTsHmTyXM4BK5ouWeG2P6kZ4nmFvuNTdtaqfobCOCg==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + } }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -28244,6 +28312,44 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "4.27.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.27.4.tgz", + "integrity": "sha512-RLKxqHEMjh/RGLsDxAEsaLO3mWgyoU6x9w6n1ikAzet4B3gI2/3yP6PWY2p9QzRTh6MfEIXB3MwsOY0Iv3vNrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.27.4", + "@rollup/rollup-android-arm64": "4.27.4", + "@rollup/rollup-darwin-arm64": "4.27.4", + "@rollup/rollup-darwin-x64": "4.27.4", + "@rollup/rollup-freebsd-arm64": "4.27.4", + "@rollup/rollup-freebsd-x64": "4.27.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.27.4", + "@rollup/rollup-linux-arm-musleabihf": "4.27.4", + "@rollup/rollup-linux-arm64-gnu": "4.27.4", + "@rollup/rollup-linux-arm64-musl": "4.27.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.27.4", + "@rollup/rollup-linux-riscv64-gnu": "4.27.4", + "@rollup/rollup-linux-s390x-gnu": "4.27.4", + "@rollup/rollup-linux-x64-gnu": "4.27.4", + "@rollup/rollup-linux-x64-musl": "4.27.4", + "@rollup/rollup-win32-arm64-msvc": "4.27.4", + "@rollup/rollup-win32-ia32-msvc": "4.27.4", + "@rollup/rollup-win32-x64-msvc": "4.27.4", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -28506,6 +28612,12 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -30527,42 +30639,6 @@ } } }, - "node_modules/tsup/node_modules/rollup": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz", - "integrity": "sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.21.2", - "@rollup/rollup-android-arm64": "4.21.2", - "@rollup/rollup-darwin-arm64": "4.21.2", - "@rollup/rollup-darwin-x64": "4.21.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.21.2", - "@rollup/rollup-linux-arm-musleabihf": "4.21.2", - "@rollup/rollup-linux-arm64-gnu": "4.21.2", - "@rollup/rollup-linux-arm64-musl": "4.21.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.21.2", - "@rollup/rollup-linux-riscv64-gnu": "4.21.2", - "@rollup/rollup-linux-s390x-gnu": "4.21.2", - "@rollup/rollup-linux-x64-gnu": "4.21.2", - "@rollup/rollup-linux-x64-musl": "4.21.2", - "@rollup/rollup-win32-arm64-msvc": "4.21.2", - "@rollup/rollup-win32-ia32-msvc": "4.21.2", - "@rollup/rollup-win32-x64-msvc": "4.21.2", - "fsevents": "~2.3.2" - } - }, "node_modules/tsup/node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", @@ -32233,42 +32309,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/vite/node_modules/rollup": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz", - "integrity": "sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.21.2", - "@rollup/rollup-android-arm64": "4.21.2", - "@rollup/rollup-darwin-arm64": "4.21.2", - "@rollup/rollup-darwin-x64": "4.21.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.21.2", - "@rollup/rollup-linux-arm-musleabihf": "4.21.2", - "@rollup/rollup-linux-arm64-gnu": "4.21.2", - "@rollup/rollup-linux-arm64-musl": "4.21.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.21.2", - "@rollup/rollup-linux-riscv64-gnu": "4.21.2", - "@rollup/rollup-linux-s390x-gnu": "4.21.2", - "@rollup/rollup-linux-x64-gnu": "4.21.2", - "@rollup/rollup-linux-x64-musl": "4.21.2", - "@rollup/rollup-win32-arm64-msvc": "4.21.2", - "@rollup/rollup-win32-ia32-msvc": "4.21.2", - "@rollup/rollup-win32-x64-msvc": "4.21.2", - "fsevents": "~2.3.2" - } - }, "node_modules/vscode-oniguruma": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", @@ -33745,42 +33785,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "packages/elements-react/node_modules/rollup": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz", - "integrity": "sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.21.2", - "@rollup/rollup-android-arm64": "4.21.2", - "@rollup/rollup-darwin-arm64": "4.21.2", - "@rollup/rollup-darwin-x64": "4.21.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.21.2", - "@rollup/rollup-linux-arm-musleabihf": "4.21.2", - "@rollup/rollup-linux-arm64-gnu": "4.21.2", - "@rollup/rollup-linux-arm64-musl": "4.21.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.21.2", - "@rollup/rollup-linux-riscv64-gnu": "4.21.2", - "@rollup/rollup-linux-s390x-gnu": "4.21.2", - "@rollup/rollup-linux-x64-gnu": "4.21.2", - "@rollup/rollup-linux-x64-musl": "4.21.2", - "@rollup/rollup-win32-arm64-msvc": "4.21.2", - "@rollup/rollup-win32-ia32-msvc": "4.21.2", - "@rollup/rollup-win32-x64-msvc": "4.21.2", - "fsevents": "~2.3.2" - } - }, "packages/elements-react/node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", @@ -33904,6 +33908,606 @@ "npm": ">=8.11.0" } }, + "packages/nextjs": { + "version": "0.0.1", + "dependencies": { + "@ory/client-fetch": "^1.15.6", + "cookie": "^1.0.1", + "psl": "^1.10.0", + "set-cookie-parser": "^2.7.1" + }, + "devDependencies": { + "@types/cookie": "^0.6.0", + "@types/psl": "^1.1.3", + "@types/set-cookie-parser": "^2.4.10", + "babel-jest": "^29.7.0", + "jest-esm-transformer": "^1.0.0", + "tsup": "8.3.0" + }, + "peerDependencies": { + "next": ">=13.1.0", + "react": ">=16.0.0" + } + }, + "packages/nextjs/node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/@ory/client-fetch": { + "version": "1.15.13", + "resolved": "https://registry.npmjs.org/@ory/client-fetch/-/client-fetch-1.15.13.tgz", + "integrity": "sha512-LjyoRkzI0FWmMAajUx9DcKdN0fBxyJs2w9tKPXcMOSWJ0x+Eahbc3jiLzKKLOCrTaqSuLLZVQZoNZKboOQesvQ==", + "license": "Apache-2.0" + }, + "packages/nextjs/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/nextjs/node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "packages/nextjs/node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "packages/nextjs/node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "packages/nextjs/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "packages/nextjs/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "packages/nextjs/node_modules/tsup": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.3.0.tgz", + "integrity": "sha512-ALscEeyS03IomcuNdFdc0YWGVIkwH1Ws7nfTbAPuoILvEV2hpGQAY72LIOjglGo4ShWpZfpBqP/jpQVCzqYQag==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.0.0", + "cac": "^6.7.14", + "chokidar": "^3.6.0", + "consola": "^3.2.3", + "debug": "^4.3.5", + "esbuild": "^0.23.0", + "execa": "^5.1.1", + "joycon": "^3.1.1", + "picocolors": "^1.0.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.19.0", + "source-map": "0.8.0-beta.0", + "sucrase": "^3.35.0", + "tinyglobby": "^0.2.1", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "packages/nextjs/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "packages/nextjs/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, "packages/preact": { "name": "@ory/elements-preact", "version": "0.0.0", diff --git a/packages/nextjs/.eslintrc.json b/packages/nextjs/.eslintrc.json new file mode 100644 index 00000000..f5d73e76 --- /dev/null +++ b/packages/nextjs/.eslintrc.json @@ -0,0 +1,25 @@ +{ + // "extends": ["../../.eslintrc.base.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": "error" + } + } + ] +} diff --git a/packages/nextjs/README.md b/packages/nextjs/README.md new file mode 100644 index 00000000..90852f26 --- /dev/null +++ b/packages/nextjs/README.md @@ -0,0 +1,17 @@ +# nextjs + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build @ory/nextjs` to build the library. + +## Developing + +Run `nx dev @ory/nextjs` to watch the source code for changes and continuously +build the library. + +## Running unit tests + +Run `nx test @ory/nextjs` to execute the unit tests via +[Jest](https://jestjs.io). diff --git a/packages/nextjs/jest.config.ts b/packages/nextjs/jest.config.ts new file mode 100644 index 00000000..526e0442 --- /dev/null +++ b/packages/nextjs/jest.config.ts @@ -0,0 +1,15 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-disable */ +export default { + displayName: "nextjs", + preset: "../../jest.preset.js", + testEnvironment: "node", + transform: { + "^.+\\.[tj]s$": ["ts-jest", { tsconfig: "/tsconfig.spec.json" }], + }, + collectCoverageFrom: ["src/**/*.ts", "src/**/*.js"], + moduleFileExtensions: ["ts", "js", "html"], + coverageDirectory: "../../coverage/packages/nextjs", +} diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json new file mode 100644 index 00000000..d65d9b4c --- /dev/null +++ b/packages/nextjs/package.json @@ -0,0 +1,73 @@ +{ + "name": "@ory/nextjs", + "version": "0.0.1", + "type": "commonjs", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "private": false, + "dependencies": { + "@ory/client-fetch": "^1.15.6", + "cookie": "^1.0.1", + "psl": "^1.10.0", + "set-cookie-parser": "^2.7.1" + }, + "devDependencies": { + "@types/cookie": "^0.6.0", + "@types/psl": "^1.1.3", + "@types/set-cookie-parser": "^2.4.10", + "babel-jest": "^29.7.0", + "jest-esm-transformer": "^1.0.0", + "tsup": "8.3.0" + }, + "keywords": [ + "ory", + "auth", + "react", + "passwordless", + "login", + "user management", + "permissions", + "authentication", + "nextjs", + "vercel", + "app router", + "pages router" + ], + "peerDependencies": { + "next": ">=13.1.0", + "react": ">=16.0.0" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./middleware": { + "types": "./dist/middleware/index.d.ts", + "import": "./dist/middleware/index.mjs", + "require": "./dist/middleware/index.js" + }, + "./app": { + "types": "./dist/app/index.d.ts", + "import": "./dist/app/index.mjs", + "require": "./dist/app/index.js" + }, + "./pages": { + "types": "./dist/pages/index.d.ts", + "import": "./dist/pages/index.mjs", + "require": "./dist/pages/index.js" + } + }, + "typesVersions": { + "*": { + "index": [ + "./dist/index.d.ts" + ], + "middleware": [ + "./dist/middleware/index.d.ts" + ] + } + } +} diff --git a/packages/nextjs/project.json b/packages/nextjs/project.json new file mode 100644 index 00000000..45ec550d --- /dev/null +++ b/packages/nextjs/project.json @@ -0,0 +1,30 @@ +{ + "name": "@ory/nextjs", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/nextjs/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "command": "tsup --clean --dts", + "options": { + "cwd": "packages/nextjs" + } + }, + "dev": { + "command": "tsup --watch --dts", + "options": { + "cwd": "packages/nextjs" + } + }, + "test": { + "executor": "@nx/jest:jest", + "dependsOn": ["build"], + "options": { + "jestConfig": "packages/nextjs/jest.config.ts", + "coverage": true, + "coverageReporters": ["text", "cobertura"] + } + } + } +} diff --git a/packages/nextjs/src/app/client.ts b/packages/nextjs/src/app/client.ts new file mode 100644 index 00000000..3fe6266f --- /dev/null +++ b/packages/nextjs/src/app/client.ts @@ -0,0 +1,15 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { Configuration, FrontendApi } from "@ory/client-fetch" + +import { orySdkUrl } from "../utils/sdk" + +export const serverSideFrontendClient = new FrontendApi( + new Configuration({ + headers: { + Accept: "application/json", + }, + basePath: orySdkUrl(), + }), +) diff --git a/packages/nextjs/src/app/flow.test.ts b/packages/nextjs/src/app/flow.test.ts new file mode 100644 index 00000000..8636c939 --- /dev/null +++ b/packages/nextjs/src/app/flow.test.ts @@ -0,0 +1,124 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { FlowType, handleFlowError } from "@ory/client-fetch" +import { + getLoginFlow, + getRecoveryFlow, + getRegistrationFlow, + getVerificationFlow, +} from "." +import { redirect } from "next/navigation" +import { getPublicUrl, toFlowParams } from "./utils" +import { serverSideFrontendClient } from "./client" + +jest.mock("./utils", () => ({ + getPublicUrl: jest.fn(), + toFlowParams: jest.fn().mockImplementation((params) => params), +})) + +jest.mock("./client", () => ({ + serverSideFrontendClient: { + getLoginFlowRaw: jest.fn(), + getRegistrationFlowRaw: jest.fn(), + getRecoveryFlowRaw: jest.fn(), + getVerificationFlowRaw: jest.fn(), + }, +})) + +jest.mock("next/navigation", () => ({ + redirect: jest.fn(), + RedirectType: { + replace: "replace", + }, +})) + +jest.mock("@ory/client-fetch", () => { + const original = jest.requireActual("@ory/client-fetch") + + return { + ...original, + handleFlowError: jest.fn(), + } +}) + +beforeEach(() => { + ;(getPublicUrl as jest.Mock).mockResolvedValue("https://example.com") + jest.clearAllMocks() + process.env["NEXT_PUBLIC_ORY_SDK_URL"] = "https://ory.sh/" + ;(handleFlowError as jest.Mock).mockReturnValue(async () => {}) +}) + +const testCases = [ + { + fn: getLoginFlow, + flowType: FlowType.Login, + m: serverSideFrontendClient.getLoginFlowRaw, + }, + { + fn: getRegistrationFlow, + flowType: FlowType.Registration, + m: serverSideFrontendClient.getRegistrationFlowRaw, + }, + { + fn: getRecoveryFlow, + flowType: FlowType.Recovery, + m: serverSideFrontendClient.getRecoveryFlowRaw, + }, + { + fn: getVerificationFlow, + flowType: FlowType.Verification, + m: serverSideFrontendClient.getVerificationFlowRaw, + }, +] + +for (const tc of testCases) { + describe(`flowtype=${tc.flowType}`, () => { + test("restarts flow if no id given", async () => { + const queryParams = {} + await tc.fn(queryParams) + expect(redirect).toHaveBeenCalledWith( + `https://example.com/self-service/${tc.flowType}/browser`, + "replace", + ) + }) + + test("restarts flow if no id is given with query params", async () => { + const queryParams = { + refresh: "true", + } + await tc.fn(queryParams) + expect(redirect).toHaveBeenCalledWith( + `https://example.com/self-service/${tc.flowType}/browser?refresh=true`, + "replace", + ) + }) + + test("fetches flow and rewrite json response", async () => { + const queryParams = { + flow: "1234", + } + ;(tc.m as jest.Mock).mockResolvedValue({ + value: jest.fn().mockResolvedValue({ + foo: "https://ory.sh/a", + bar: "https://ory.sh/", + }), + } as any) + const result = await tc.fn(queryParams) + expect(result).toEqual({ + foo: "https://example.com/a", + bar: "https://example.com/", + }) + }) + + test("fetches flow and calls error handler on error", async () => { + const queryParams = { + flow: "1234", + } + ;(tc.m as jest.Mock).mockRejectedValue(new Error("error")) + const result = await tc.fn(queryParams) + expect(result).toBeNull() + expect(handleFlowError).toHaveBeenCalled() + }) + }) +} diff --git a/packages/nextjs/src/app/flow.ts b/packages/nextjs/src/app/flow.ts new file mode 100644 index 00000000..f52d2ed1 --- /dev/null +++ b/packages/nextjs/src/app/flow.ts @@ -0,0 +1,53 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { redirect, RedirectType } from "next/navigation" +import { FlowType, handleFlowError } from "@ory/client-fetch" + +import { getPublicUrl, onRedirect } from "./utils" +import { QueryParams } from "../types" +import { guessPotentiallyProxiedOrySdkUrl } from "../utils/sdk" +import { onValidationError } from "../utils/utils" +import { rewriteJsonResponse } from "../utils/rewrite" +import * as runtime from "@ory/client-fetch/src/runtime" + +export async function getFlow( + params: QueryParams, + fetchFlowRaw: () => Promise>, + flowType: FlowType, +): Promise { + // Guess our own public url using Next.js helpers. We need the hostname, port, and protocol. + const knownProxiedUrl = await getPublicUrl() + const url = guessPotentiallyProxiedOrySdkUrl({ + knownProxiedUrl, + }) + + const onRestartFlow = () => { + const redirectTo = new URL( + "/self-service/" + flowType.toString() + "/browser", + url, + ) + redirectTo.search = new URLSearchParams(params).toString() + return redirect(redirectTo.toString(), RedirectType.replace) + } + + if (!params["flow"]) { + onRestartFlow() + return + } + + try { + const rawResponse = await fetchFlowRaw() + return await rawResponse + .value() + .then((v: T): T => rewriteJsonResponse(v, url)) + } catch (error) { + const errorHandler = handleFlowError({ + onValidationError, + onRestartFlow, + onRedirect: onRedirect, + }) + await errorHandler(error) + return null + } +} diff --git a/packages/nextjs/src/app/index.ts b/packages/nextjs/src/app/index.ts new file mode 100644 index 00000000..18e19f5b --- /dev/null +++ b/packages/nextjs/src/app/index.ts @@ -0,0 +1,10 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 +"use server" + +export { getLoginFlow } from "./login" +export { getRegistrationFlow } from "./registration" +export { getRecoveryFlow } from "./recovery" +export { getVerificationFlow } from "./verification" + +export type { OryPageParams } from "./utils" diff --git a/packages/nextjs/src/app/login.ts b/packages/nextjs/src/app/login.ts new file mode 100644 index 00000000..34b8451e --- /dev/null +++ b/packages/nextjs/src/app/login.ts @@ -0,0 +1,53 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 +import { FlowType, LoginFlow } from "@ory/client-fetch" + +import { initOverrides, QueryParams } from "../types" +import { serverSideFrontendClient } from "./client" +import { getFlow } from "./flow" +import { toFlowParams } from "./utils" + +/** + * Use this method in an app router page to fetch an existing login flow or to create a new one. This method works with server-side rendering. + * + * ``` + * import { Login } from "@ory/elements-react/theme" + * import { getLoginFlow, OryPageParams } from "@ory/nextjs/app" + * import { enhanceConfig } from "@ory/nextjs" + * + * import config from "@/ory.config" + * import CardHeader from "@/app/auth/login/card-header" + * + * export default async function LoginPage(props: OryPageParams) { + * const flow = await getLoginFlow(props.searchParams) + * + * if (!flow) { + * return null + * } + * + * return ( + * + * ) + * } + * ``` + * + * @param params The query parameters of the request. + */ +export async function getLoginFlow( + params: QueryParams | Promise, +): Promise { + const p = await toFlowParams(await params) + return getFlow( + params, + () => serverSideFrontendClient.getLoginFlowRaw(p, initOverrides), + FlowType.Login, + ) +} diff --git a/packages/nextjs/src/app/recovery.ts b/packages/nextjs/src/app/recovery.ts new file mode 100644 index 00000000..c25dd902 --- /dev/null +++ b/packages/nextjs/src/app/recovery.ts @@ -0,0 +1,53 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 +import { FlowType, RecoveryFlow } from "@ory/client-fetch" + +import { initOverrides, QueryParams } from "../types" +import { serverSideFrontendClient } from "./client" +import { getFlow } from "./flow" +import { toFlowParams } from "./utils" + +/** + * Use this method in an app router page to fetch an existing recovery flow or to create a new one. This method works with server-side rendering. + * + * ``` + * import { Recovery } from "@ory/elements-react/theme" + * import { getRecoveryFlow, OryPageParams } from "@ory/nextjs/app" + * import { enhanceConfig } from "@ory/nextjs" + * + * import config from "@/ory.config" + * import CardHeader from "@/app/auth/recovery/card-header" + * + * export default async function RecoveryPage(props: OryPageParams) { + * const flow = await getRecoveryFlow(props.searchParams) + * + * if (!flow) { + * return null + * } + * + * return ( + * + * ) + * } + * ``` + * + * @param params The query parameters of the request. + */ +export async function getRecoveryFlow( + params: QueryParams | Promise, +): Promise { + const p = await toFlowParams(await params) + return getFlow( + params, + () => serverSideFrontendClient.getRecoveryFlowRaw(p, initOverrides), + FlowType.Recovery, + ) +} diff --git a/packages/nextjs/src/app/registration.ts b/packages/nextjs/src/app/registration.ts new file mode 100644 index 00000000..f9f4337e --- /dev/null +++ b/packages/nextjs/src/app/registration.ts @@ -0,0 +1,53 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 +import { FlowType, RegistrationFlow } from "@ory/client-fetch" + +import { initOverrides, QueryParams } from "../types" +import { serverSideFrontendClient } from "./client" +import { getFlow } from "./flow" +import { toFlowParams } from "./utils" + +/** + * Use this method in an app router page to fetch an existing registration flow or to create a new one. This method works with server-side rendering. + * + * ``` + * import { Registration } from "@ory/elements-react/theme" + * import { getRegistrationFlow, OryPageParams } from "@ory/nextjs/app" + * import { enhanceConfig } from "@ory/nextjs" + * + * import config from "@/ory.config" + * import CardHeader from "@/app/auth/registration/card-header" + * + * export default async function RegistrationPage(props: OryPageParams) { + * const flow = await getRegistrationFlow(props.searchParams) + * + * if (!flow) { + * return null + * } + * + * return ( + * + * ) + * } + * ``` + * + * @param params The query parameters of the request. + */ +export async function getRegistrationFlow( + params: QueryParams | Promise, +): Promise { + const p = await toFlowParams(await params) + return getFlow( + params, + () => serverSideFrontendClient.getRegistrationFlowRaw(p, initOverrides), + FlowType.Registration, + ) +} diff --git a/packages/nextjs/src/app/utils.test.ts b/packages/nextjs/src/app/utils.test.ts new file mode 100644 index 00000000..50ec267c --- /dev/null +++ b/packages/nextjs/src/app/utils.test.ts @@ -0,0 +1,82 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { headers } from "next/headers" + +import { getCookieHeader, getPublicUrl } from "./utils" + +// Mocking dependencies +jest.mock("next/headers", () => ({ + headers: jest.fn(), +})) + +jest.mock("next/navigation", () => ({ + redirect: jest.fn(), +})) + +jest.mock("../utils/utils", () => ({ + toFlowParams: jest.fn(), +})) + +describe("getCookieHeader", () => { + it("should return the 'cookie' header if present", async () => { + const headersMock = { + get: jest.fn().mockReturnValue("cookie-value"), + } + ;(headers as jest.Mock).mockResolvedValue(headersMock) + + const result = await getCookieHeader() + expect(headersMock.get).toHaveBeenCalledWith("cookie") + expect(result).toBe("cookie-value") + }) + + it("should return undefined if the 'cookie' header is not present", async () => { + const headersMock = { + get: jest.fn().mockReturnValue(undefined), + } + ;(headers as jest.Mock).mockResolvedValue(headersMock) + + const result = await getCookieHeader() + expect(headersMock.get).toHaveBeenCalledWith("cookie") + expect(result).toBeUndefined() + }) +}) + +describe("getPublicUrl", () => { + it("should construct the URL with the x-forwarded-proto header when available", async () => { + const headersMock = { + get: jest.fn((key: string) => { + if (key === "host") return "example.com" + if (key === "x-forwarded-proto") return "https" + return undefined + }), + } + ;(headers as jest.Mock).mockResolvedValue(headersMock) + + const result = await getPublicUrl() + expect(result).toBe("https://example.com") + }) + + it("should default to http if x-forwarded-proto is not present", async () => { + const headersMock = { + get: jest.fn((key: string) => { + if (key === "host") return "example.com" + return undefined + }), + } + ;(headers as jest.Mock).mockResolvedValue(headersMock) + + const result = await getPublicUrl() + expect(result).toBe("http://example.com") + }) + + it("should handle missing host header gracefully", async () => { + const headersMock = { + get: jest.fn().mockReturnValue(undefined), + } + ;(headers as jest.Mock).mockResolvedValue(headersMock) + + const result = await getPublicUrl() + expect(result).toBe("http://undefined") + }) +}) diff --git a/packages/nextjs/src/app/utils.ts b/packages/nextjs/src/app/utils.ts new file mode 100644 index 00000000..c9a68960 --- /dev/null +++ b/packages/nextjs/src/app/utils.ts @@ -0,0 +1,32 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 +import { headers } from "next/headers" +import { redirect } from "next/navigation" +import { OnRedirectHandler } from "@ory/client-fetch" + +import { QueryParams } from "../types" +import { toFlowParams as baseToFlowParams } from "../utils/utils" + +export async function getCookieHeader() { + const h = await headers() + return h.get("cookie") ?? undefined +} + +export const onRedirect: OnRedirectHandler = (url) => { + redirect(url) +} + +export async function toFlowParams(params: QueryParams) { + return baseToFlowParams(params, getCookieHeader) +} + +export async function getPublicUrl() { + const h = await headers() + const host = h.get("host") + const protocol = h.get("x-forwarded-proto") || "http" + return `${protocol}://${host}` +} + +export interface OryPageParams { + searchParams: Promise +} diff --git a/packages/nextjs/src/app/verification.ts b/packages/nextjs/src/app/verification.ts new file mode 100644 index 00000000..db007dc5 --- /dev/null +++ b/packages/nextjs/src/app/verification.ts @@ -0,0 +1,53 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 +import { FlowType, VerificationFlow } from "@ory/client-fetch" + +import { initOverrides, QueryParams } from "../types" +import { serverSideFrontendClient } from "./client" +import { getFlow } from "./flow" +import { toFlowParams } from "./utils" + +/** + * Use this method in an app router page to fetch an existing verification flow or to create a new one. This method works with server-side rendering. + * + * ``` + * import { Verification } from "@ory/elements-react/theme" + * import { getVerificationFlow, OryPageParams } from "@ory/nextjs/app" + * import { enhanceConfig } from "@ory/nextjs" + * + * import config from "@/ory.config" + * import CardHeader from "@/app/auth/verification/card-header" + * + * export default async function VerificationPage(props: OryPageParams) { + * const flow = await getVerificationFlow(props.searchParams) + * + * if (!flow) { + * return null + * } + * + * return ( + * + * ) + * } + * ``` + * + * @param params The query parameters of the request. + */ +export async function getVerificationFlow( + params: QueryParams | Promise, +): Promise { + const p = await toFlowParams(await params) + return getFlow( + params, + () => serverSideFrontendClient.getVerificationFlowRaw(p, initOverrides), + FlowType.Verification, + ) +} diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts new file mode 100644 index 00000000..c0702f91 --- /dev/null +++ b/packages/nextjs/src/index.ts @@ -0,0 +1,5 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +export type { OryConfig } from "./types" +export { enhanceOryConfig } from "./utils/config" diff --git a/packages/nextjs/src/middleware/index.ts b/packages/nextjs/src/middleware/index.ts new file mode 100644 index 00000000..e65dbc7b --- /dev/null +++ b/packages/nextjs/src/middleware/index.ts @@ -0,0 +1,4 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +export { createOryMiddleware } from "./middleware" diff --git a/packages/nextjs/src/middleware/middleware.test.ts b/packages/nextjs/src/middleware/middleware.test.ts new file mode 100644 index 00000000..890d877f --- /dev/null +++ b/packages/nextjs/src/middleware/middleware.test.ts @@ -0,0 +1,259 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { proxyRequest } from "./middleware" +import { NextRequest, NextResponse } from "next/server" +import { OryConfig } from "../types" + +function stringToReadableStream(input: string): ReadableStream { + return new Blob([input]).stream() +} + +// Mocking the NextURL class to simulate the behavior of nextUrl in NextRequest +class MockNextURL { + public pathname + public protocol + public host + public origin + + constructor(public url: string) { + const parsed = new URL(url) + this.pathname = parsed.pathname + this.protocol = parsed.protocol + this.host = parsed.host + this.origin = parsed.origin + } + + clone() { + return new MockNextURL(this.url) + } +} + +// Updated createMockRequest function to use MockNextURL +const createMockRequest = ( + url: string, + options: Partial = {}, +): NextRequest => { + return { + nextUrl: new MockNextURL(url) as unknown as NextRequest["nextUrl"], // Cast to NextRequest's nextUrl type + method: options.method || "GET", + headers: new Headers(options.headers || {}), + arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(0)), + ...options, + } as NextRequest +} + +const mockFetch = (responseInit: Partial) => { + global.fetch = jest.fn().mockResolvedValue( + new Response(responseInit.body || "", { + headers: new Headers(responseInit.headers || {}), + status: responseInit.status || 200, + }), + ) +} + +const createOptions = (): OryConfig => ({ + forwardAdditionalHeaders: ["x-custom-header"], + override: { + loginUiPath: "/custom-login", + }, +}) + +function createMockLoginRequest( + path: string = "/self-service/login", + headers: HeadersInit = [], + protocol: string = "http", +) { + return createMockRequest(`${protocol}://localhost${path}`, { + headers: new Headers({ + host: "localhost", + ...headers, + }), + }) +} + +describe("proxyRequest", () => { + beforeEach(() => { + jest.resetAllMocks() + process.env["NEXT_PUBLIC_ORY_SDK_URL"] = + "https://playground.projects.oryapis.com" + process.env["NODE_ENV"] = "development" + }) + + afterAll(() => { + delete process.env["NEXT_PUBLIC_ORY_SDK_URL"] + delete process.env["NODE_ENV"] + }) + + it("proxies a request and modifies the set-cookie header", async () => { + const request = createMockLoginRequest() + const upstreamResponseHeaders = new Headers({ + "set-cookie": + "session=a; Domain=playground.projects.oryapis.com; Path=/; HttpOnly", + "content-type": "application/json", + }) + + mockFetch({ + headers: upstreamResponseHeaders, + }) + + const response = await proxyRequest(request, createOptions()) + + expect(response).toBeInstanceOf(NextResponse) + expect(response?.headers.get("set-cookie")).toEqual( + "session=a; Domain=localhost; Path=/; HttpOnly", + ) + expect(response?.headers.get("content-type")).toBe("application/json") + }) + + it("proxies a request and modifies the JSON response", async () => { + const request = createMockLoginRequest() + const upstreamResponseHeaders = new Headers({ + "content-type": "application/json", + }) + + mockFetch({ + headers: upstreamResponseHeaders, + body: stringToReadableStream( + JSON.stringify({ + action: "https://playground.projects.oryapis.com/self-service/login", + }), + ), + }) + + const response = await proxyRequest(request, createOptions()) + + expect(response).toBeInstanceOf(NextResponse) + expect(response?.headers.get("content-type")).toBe("application/json") + const body = await response?.text() + expect(body).toEqual( + JSON.stringify({ + action: "http://localhost/self-service/login", + }), + ) + }) + + it("proxies a request and modifies the HTML", async () => { + const request = createMockLoginRequest() + + mockFetch({ + headers: new Headers({ + "content-type": "text/html", + }), + body: stringToReadableStream( + "logout", + ), + }) + + const options = createOptions() + const response = await proxyRequest(request, options) + + expect(response).toBeInstanceOf(NextResponse) + expect(response?.headers.get("content-type")).toBe("text/html") + const body = await response?.text() + expect(body).toEqual( + "logout", + ) + }) + + it("modifies location header for redirects", async () => { + const request = createMockLoginRequest() + const upstreamResponseHeaders = new Headers({ + location: "https://playground.projects.oryapis.com/self-service/login", + }) + + mockFetch({ + headers: upstreamResponseHeaders, + status: 302, + }) + + const response = await proxyRequest(request, createOptions()) + + expect(response?.headers.get("location")).toBe( + "http://localhost/self-service/login", + ) + expect(response?.status).toBe(302) + }) + + const createTestCase = ( + part: "login" | "registration" | "recovery" | "verification" | "settings", + ) => ({ + path: `/ui/${part}`, + override: { + [`${part}UiPath`]: `/custom/${part}`, + }, + expect: `/custom/${part}`, + }) + + it.each([ + createTestCase("login"), + createTestCase("registration"), + createTestCase("recovery"), + createTestCase("verification"), + createTestCase("settings"), + ])( + "modifies location header for redirects with custom ory elements page overrides $path", + async ({ override, path, expect: expectUrl }) => { + const request = createMockLoginRequest() + const upstreamResponseHeaders = new Headers({ + location: "https://playground.projects.oryapis.com" + path, + }) + + mockFetch({ + headers: upstreamResponseHeaders, + status: 302, + }) + + const response = await proxyRequest(request, { + override, + }) + + expect(response?.headers.get("location")).toBe( + "http://localhost" + expectUrl, + ) + expect(response?.status).toBe(302) + }, + ) + + it("bypasses requests that do not match proxy paths", async () => { + const request = createMockRequest("http://localhost/non-proxy-path") + const body = + "logout" + + mockFetch({ + body: stringToReadableStream(body), + }) + + const response = await proxyRequest(request, createOptions()) + const got = await response?.text() + expect(got).toEqual("") + }) + + it("preserves additional forwarded headers", async () => { + const request = createMockLoginRequest(undefined, { + "x-custom-header": "test-value", + authorization: "Bearer token", + }) + + const fetch = jest.fn().mockResolvedValue( + new Response( + "https://playground.projects.oryapis.com/self-service/login", + { + status: 200, + }, + ), + ) + global.fetch = fetch + + const response = await proxyRequest(request, createOptions()) + + expect(fetch).toHaveBeenCalled() + const fetchArgs = fetch.mock.calls[0][1] + + expect(fetchArgs.headers.get("x-custom-header")).toBe("test-value") + expect(fetchArgs.headers.get("authorization")).toBe("Bearer token") + + const body = await response?.text() + expect(body).toEqual("http://localhost/self-service/login") + }) +}) diff --git a/packages/nextjs/src/middleware/middleware.ts b/packages/nextjs/src/middleware/middleware.ts new file mode 100644 index 00000000..cfc7344d --- /dev/null +++ b/packages/nextjs/src/middleware/middleware.ts @@ -0,0 +1,142 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { NextResponse, type NextRequest } from "next/server" + +import { rewriteUrls } from "../utils/rewrite" +import { filterRequestHeaders, processSetCookieHeaders } from "../utils/utils" +import { OryConfig } from "../types" +import { defaultOmitHeaders } from "../utils/headers" +import { orySdkUrl } from "../utils/sdk" + +export function getProjectApiKey() { + let baseUrl = "" + + if (process.env["ORY_PROJECT_API_TOKEN"]) { + baseUrl = process.env["ORY_PROJECT_API_TOKEN"] + } + + return baseUrl.replace(/\/$/, "") +} + +export async function proxyRequest(request: NextRequest, options: OryConfig) { + const match = [ + "/self-service", + "/sessions/whoami", + "/ui", + "/.well-known/ory", + "/.ory", + ] + if (!match.some((m) => request.nextUrl.pathname.startsWith(m))) { + return NextResponse.next() + } + + const matchBaseUrl = new URL(orySdkUrl()) + const selfUrl = request.nextUrl.protocol + "//" + request.nextUrl.host + + const upstreamUrl = request.nextUrl.clone() + upstreamUrl.hostname = matchBaseUrl.hostname + upstreamUrl.host = matchBaseUrl.host + upstreamUrl.protocol = matchBaseUrl.protocol + upstreamUrl.port = matchBaseUrl.port + + const upstreamRequestHeaders = filterRequestHeaders( + request.headers, + options.forwardAdditionalHeaders, + ) + upstreamRequestHeaders.set("Host", upstreamUrl.hostname) + + // Ensures we use the correct URL in redirects like OIDC redirects. + upstreamRequestHeaders.set("Ory-Base-URL-Rewrite", selfUrl.toString()) + upstreamRequestHeaders.set("Ory-Base-URL-Rewrite-Token", getProjectApiKey()) + + // We disable custom domain redirects. + upstreamRequestHeaders.set("Ory-No-Custom-Domain-Redirect", "true") + + // Fetch the upstream response + const upstreamResponse = await fetch(upstreamUrl.toString(), { + method: request.method, + headers: upstreamRequestHeaders, + body: + request.method !== "GET" && request.method !== "HEAD" + ? await request.arrayBuffer() + : null, + redirect: "manual", + }) + + // Delete headers that should not be forwarded + defaultOmitHeaders.forEach((header) => { + upstreamResponse.headers.delete(header) + }) + + // Modify cookie domain + if (upstreamResponse.headers.get("set-cookie")) { + const cookies = processSetCookieHeaders( + request.nextUrl.protocol, + upstreamResponse, + options, + request.headers, + ) + upstreamResponse.headers.delete("set-cookie") + cookies.forEach((cookie) => { + upstreamResponse.headers.append("Set-Cookie", cookie) + }) + } + + // Modify location header + const originalLocation = upstreamResponse.headers.get("location") + if (originalLocation) { + let location = originalLocation + + // The legacy hostedui does a redirect to `../self-service` which breaks the NextJS middleware. + // To fix this, we hard-rewrite `../self-service`. + // + // This is not needed with the "new" account experience based on this SDK. + if (location.startsWith("../self-service")) { + location = location.replace("../self-service", "/self-service") + } + + location = rewriteUrls(location, matchBaseUrl.toString(), selfUrl, options) + + if (!location.startsWith("http")) { + // console.debug('rewriting location', selfUrl, location, new URL(location, selfUrl).toString()) + location = new URL(location, selfUrl).toString() + } + + // Next.js throws an error that is completely unhelpful if the location header is not an absolute URL. + // Therefore, we throw a more helpful error message here. + if (!location.startsWith("http")) { + throw new Error( + "The HTTP location header must be an absolute URL in NextJS middlewares. However, it is not. The resulting HTTP location is `" + + location + + "`. This is either a configuration or code bug. Please open an issue on https://github.com/ory/elements.", + ) + } + + upstreamResponse.headers.set("location", location) + } + + // Modify buffer + let modifiedBody = Buffer.from(await upstreamResponse.arrayBuffer()) + if ( + upstreamResponse.headers.get("content-type")?.includes("text/") || + upstreamResponse.headers.get("content-type")?.includes("application/json") + ) { + const bufferString = modifiedBody.toString("utf-8") + modifiedBody = Buffer.from( + rewriteUrls(bufferString, matchBaseUrl.toString(), selfUrl, options), + ) + } + + // Return the modified response + return new NextResponse(modifiedBody, { + headers: upstreamResponse.headers, + status: upstreamResponse.status, + }) +} + +export function createOryMiddleware(options: OryConfig) { + return (r: NextRequest) => { + return proxyRequest(r, options) + } +} diff --git a/packages/nextjs/src/pages/client.ts b/packages/nextjs/src/pages/client.ts new file mode 100644 index 00000000..9cc0359d --- /dev/null +++ b/packages/nextjs/src/pages/client.ts @@ -0,0 +1,18 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 +import { Configuration, FrontendApi } from "@ory/client-fetch" + +import { guessPotentiallyProxiedOrySdkUrl } from "../utils/sdk" + +export const clientSideFrontendClient = () => + new FrontendApi( + new Configuration({ + headers: { + Accept: "application/json", + }, + credentials: "include", + basePath: guessPotentiallyProxiedOrySdkUrl({ + knownProxiedUrl: window.location.origin, + }), + }), + ) diff --git a/packages/nextjs/src/pages/flow.ts b/packages/nextjs/src/pages/flow.ts new file mode 100644 index 00000000..ebc27cb2 --- /dev/null +++ b/packages/nextjs/src/pages/flow.ts @@ -0,0 +1,65 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { FlowType, handleFlowError } from "@ory/client-fetch" +import { useEffect, useState } from "react" +import { useRouter } from "next/router" +import { useSearchParams } from "next/navigation" +import { handleRestartFlow, onValidationError, useOnRedirect } from "./utils" +import { toValue } from "../utils/utils" +import * as runtime from "@ory/client-fetch/src/runtime" + +interface Flow { + id: string +} + +export function createUseFlowFactory( + flowType: FlowType, + createFlow: (params: URLSearchParams) => Promise>, + getFlow: (id: string) => Promise>, +): () => T | null | void { + return () => { + const [flow, setFlow] = useState() + const router = useRouter() + const searchParams = useSearchParams() + const onRestartFlow = handleRestartFlow(searchParams, flowType) + const onRedirect = useOnRedirect() + + const errorHandler = handleFlowError({ + onValidationError, + onRestartFlow, + onRedirect, + }) + + const handleSetFlow = async (flow: T) => { + setFlow(flow) + + // Use the router to update the `flow` search parameter only + await router.replace({ + query: { flow: flow.id }, + }) + return + } + + useEffect(() => { + const id = searchParams.get("flow") + + // If the router is not ready yet, or we already have a flow, do nothing. + if (!router.isReady || flow !== undefined) { + return + } + + if (!id) { + createFlow(searchParams) + .then(toValue) + .then(handleSetFlow) + .catch(errorHandler) + return + } + + getFlow(id).then(toValue).then(handleSetFlow).catch(errorHandler) + }, [searchParams, router, router.isReady, flow]) + + return flow + } +} diff --git a/packages/nextjs/src/pages/index.ts b/packages/nextjs/src/pages/index.ts new file mode 100644 index 00000000..e2a8c6ae --- /dev/null +++ b/packages/nextjs/src/pages/index.ts @@ -0,0 +1,8 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 +"use client" + +export { useRegistrationFlow } from "./registration" +export { useVerificationFlow } from "./verification" +export { useRecoveryFlow } from "./recovery" +export { useLoginFlow } from "./login" diff --git a/packages/nextjs/src/pages/login.ts b/packages/nextjs/src/pages/login.ts new file mode 100644 index 00000000..4a848a23 --- /dev/null +++ b/packages/nextjs/src/pages/login.ts @@ -0,0 +1,22 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { clientSideFrontendClient } from "./client" +import { createUseFlowFactory } from "./flow" +import { FlowType } from "@ory/client-fetch" + +export const useLoginFlow = createUseFlowFactory( + FlowType.Login, + (params: URLSearchParams) => { + return clientSideFrontendClient().createBrowserLoginFlowRaw({ + refresh: params.get("refresh") === "true", + aal: params.get("aal") ?? undefined, + returnTo: params.get("return_to") ?? undefined, + cookie: params.get("cookie") ?? undefined, + loginChallenge: params.get("login_challenge") ?? undefined, + organization: params.get("organization") ?? undefined, + via: params.get("via") ?? undefined, + }) + }, + (id) => clientSideFrontendClient().getLoginFlowRaw({ id }), +) diff --git a/packages/nextjs/src/pages/recovery.ts b/packages/nextjs/src/pages/recovery.ts new file mode 100644 index 00000000..81671580 --- /dev/null +++ b/packages/nextjs/src/pages/recovery.ts @@ -0,0 +1,16 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { clientSideFrontendClient } from "./client" +import { createUseFlowFactory } from "./flow" +import { FlowType } from "@ory/client-fetch" + +export const useRecoveryFlow = createUseFlowFactory( + FlowType.Recovery, + (params: URLSearchParams) => { + return clientSideFrontendClient().createBrowserRecoveryFlowRaw({ + returnTo: params.get("return_to") ?? undefined, + }) + }, + (id) => clientSideFrontendClient().getRecoveryFlowRaw({ id }), +) diff --git a/packages/nextjs/src/pages/registration.ts b/packages/nextjs/src/pages/registration.ts new file mode 100644 index 00000000..1d3d9c4c --- /dev/null +++ b/packages/nextjs/src/pages/registration.ts @@ -0,0 +1,20 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { clientSideFrontendClient } from "./client" +import { createUseFlowFactory } from "./flow" +import { FlowType } from "@ory/client-fetch" + +export const useRegistrationFlow = createUseFlowFactory( + FlowType.Registration, + (params: URLSearchParams) => { + return clientSideFrontendClient().createBrowserRegistrationFlowRaw({ + returnTo: params.get("return_to") ?? undefined, + loginChallenge: params.get("registration_challenge") ?? undefined, + afterVerificationReturnTo: + params.get("after_verification_return_to") ?? undefined, + organization: params.get("organization") ?? undefined, + }) + }, + (id) => clientSideFrontendClient().getRegistrationFlowRaw({ id }), +) diff --git a/packages/nextjs/src/pages/utils.ts b/packages/nextjs/src/pages/utils.ts new file mode 100644 index 00000000..5cbf7e8d --- /dev/null +++ b/packages/nextjs/src/pages/utils.ts @@ -0,0 +1,38 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { FlowType, OnRedirectHandler } from "@ory/client-fetch" +import { guessPotentiallyProxiedOrySdkUrl } from "../utils/sdk" +import { useRouter } from "next/router" + +export function onValidationError(value: T): T { + return value +} + +export const toBrowserEndpointRedirect = ( + params: URLSearchParams, + flowType: FlowType, +) => + guessPotentiallyProxiedOrySdkUrl({ + knownProxiedUrl: window.location.origin, + }) + + "/self-service/" + + flowType.toString() + + "/browser?" + + new URLSearchParams(params).toString() + +export const handleRestartFlow = + (searchParams: URLSearchParams, flowType: FlowType) => () => { + window.location.assign(toBrowserEndpointRedirect(searchParams, flowType)) + } + +export function useOnRedirect(): OnRedirectHandler { + const router = useRouter() + return (url: string, external: boolean) => { + if (external) { + window.location.assign(url) + } else { + router.push(url) + } + } +} diff --git a/packages/nextjs/src/pages/verification.ts b/packages/nextjs/src/pages/verification.ts new file mode 100644 index 00000000..5c1a513d --- /dev/null +++ b/packages/nextjs/src/pages/verification.ts @@ -0,0 +1,16 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { clientSideFrontendClient } from "./client" +import { createUseFlowFactory } from "./flow" +import { FlowType } from "@ory/client-fetch" + +export const useVerificationFlow = createUseFlowFactory( + FlowType.Verification, + (params: URLSearchParams) => { + return clientSideFrontendClient().createBrowserVerificationFlowRaw({ + returnTo: params.get("return_to") ?? undefined, + }) + }, + (id) => clientSideFrontendClient().getVerificationFlowRaw({ id }), +) diff --git a/packages/nextjs/src/types.ts b/packages/nextjs/src/types.ts new file mode 100644 index 00000000..a49fb600 --- /dev/null +++ b/packages/nextjs/src/types.ts @@ -0,0 +1,88 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +export interface OryConfig { + /** + * Sets the base path for proxying requests to Ory during development and previews. Is unset per default for best + * compatibility. + * + * For example, Ory's `/self-service/login/browser` API will be proxied in your application at `/self-service/login/browser`. + * This proxying is only enabled in development and preview deployments and disabled in production. + * + * If you want to proxy Ory's `/self-service/login/browser` API at `/api/self-service/login/browser`, you can set this option to `/api`. + */ + proxyBasePath?: string + + /** + * Per default, this handler will strip the cookie domain from + * the Set-Cookie instruction which is recommended for most set ups. + * + * If you are running this app on a subdomain and you want the session and CSRF cookies + * to be valid for the whole TLD, you can use this setting to force a cookie domain. + * + * Please be aware that his method disables the `dontUseTldForCookieDomain` option. + */ + forceCookieDomain?: string + + /** + * Per default headers are filtered to forward only a fixed list. + * + * If you need to forward additional headers you can use this setting to define them. + */ + forwardAdditionalHeaders?: string[] + + /** + * Override the default UI for login, registration, recovery, verification, and settings flows with a page + * in your project. This is useful if you want to customize the UI for these flows. + */ + override?: { + applicationName?: string + + /** + * Set this to use a custom login UI for the login flow. This path should be relative to the + * project root. Assuming you have a file at `./app/my-login/page.tsx`, you would set this to + * `/my-login`. + */ + loginUiPath?: string + + /** + * Set this to use a custom registration UI for the registration flow. This path should be relative to the + * project root. Assuming you have a file at `./app/my-registration/page.tsx`, you would set this to + * `/my-registration`. + */ + registrationUiPath?: string + + /** + * Set this to use a custom recovery UI for the recovery flow. This path should be relative to the + * project root. Assuming you have a file at `./app/my-recovery/page.tsx`, you would set this to + * `/my-recovery`. + */ + recoveryUiPath?: string + + /** + * Set this to use a custom verification UI for the verification flow. This path should be relative to the + * project root. Assuming you have a file at `./app/my-verification/page.tsx`, you would set this to + * `/my-verification`. + */ + verificationUiPath?: string + + /** + * Set this to use a custom settings UI for the settings flow. This path should be relative to the + * project root. Assuming you have a file at `./app/my-settings/page.tsx`, you would set this to + * `/my-settings`. + */ + settingsUiPath?: string + } +} + +export type QueryParams = { [key: string]: any } + +export const initOverrides: RequestInit = { + cache: "no-cache", +} + +export type FlowParams = { + id: string + cookie: string | undefined + return_to: string +} diff --git a/packages/nextjs/src/utils/__snapshots__/utils.test.ts.snap b/packages/nextjs/src/utils/__snapshots__/utils.test.ts.snap new file mode 100644 index 00000000..92498c13 --- /dev/null +++ b/packages/nextjs/src/utils/__snapshots__/utils.test.ts.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`processSetCookieHeaders should respect forwarded headers 1`] = ` +[ + "sessionid=abc123; Domain=ory.sh; Path=/; HttpOnly; Secure", +] +`; + +exports[`processSetCookieHeaders should respect regular headers 1`] = ` +[ + "sessionid=abc123; Domain=ory.sh; Path=/; HttpOnly", +] +`; + +exports[`processSetCookieHeaders supports insecure 1`] = ` +[ + "sessionid=abc123; Domain=ory.sh; Path=/; HttpOnly", +] +`; + +exports[`processSetCookieHeaders supports multiple cookies comma separated 1`] = ` +[ + "sessionid1=abc123; Domain=ory.sh; Path=/; HttpOnly", + "sessionid2=123abc; Domain=ory.sh; Path=/abc; HttpOnly", +] +`; + +exports[`processSetCookieHeaders supports multiple cookies in record 1`] = ` +[ + "sessionid1=abc123; Domain=ory.sh; Path=/; HttpOnly", + "sessionid2=123abc; Domain=ory.sh; Path=/abc; HttpOnly", +] +`; diff --git a/packages/nextjs/src/utils/config.test.ts b/packages/nextjs/src/utils/config.test.ts new file mode 100644 index 00000000..2e8d4b70 --- /dev/null +++ b/packages/nextjs/src/utils/config.test.ts @@ -0,0 +1,90 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { enhanceOryConfig } from "./config" +import { isProduction } from "./sdk" +import { OryConfig } from "../types" + +jest.mock("./sdk", () => ({ + isProduction: jest.fn(), +})) + +describe("enhanceConfig", () => { + const originalEnv = process.env + + beforeEach(() => { + jest.resetModules() + process.env = { ...originalEnv } + }) + + afterEach(() => { + process.env = originalEnv + }) + + it("should use forceSdkUrl if provided", () => { + const config: Partial = {} + const result = enhanceOryConfig(config, "https://forced-url.com") + expect(result.sdk.url).toBe("https://forced-url.com") + }) + + it("should use NEXT_PUBLIC_ORY_SDK_URL if forceSdkUrl is not provided", () => { + process.env["NEXT_PUBLIC_ORY_SDK_URL"] = "https://public-sdk-url.com" + const config: Partial = {} + const result = enhanceOryConfig(config) + expect(result.sdk.url).toBe("https://public-sdk-url.com") + }) + + it("should use ORY_SDK_URL if NEXT_PUBLIC_ORY_SDK_URL is not provided", () => { + process.env["ORY_SDK_URL"] = "https://sdk-url.com" + const config: Partial = {} + const result = enhanceOryConfig(config) + expect(result.sdk.url).toBe("https://sdk-url.com") + }) + + it("should use __NEXT_PRIVATE_ORIGIN if not in production and forceSdkUrl is not provided", () => { + ;(isProduction as jest.Mock).mockReturnValue(false) + process.env["__NEXT_PRIVATE_ORIGIN"] = "https://private-origin.com/" + const config: Partial = {} + const result = enhanceOryConfig(config) + expect(result.sdk.url).toBe("https://private-origin.com") + }) + + it("should use VERCEL_URL if __NEXT_PRIVATE_ORIGIN is not provided", () => { + ;(isProduction as jest.Mock).mockReturnValue(false) + process.env["VERCEL_URL"] = "vercel-url.com" + const config: Partial = {} + const result = enhanceOryConfig(config) + expect(result.sdk.url).toBe("https://vercel-url.com") + }) + + xit("should use window.location.origin if VERCEL_URL is not provided", () => { + // Not sure if this works + ;(isProduction as jest.Mock).mockReturnValue(false) + delete process.env["VERCEL_URL"] + const config: Partial = {} + const windowSpy = jest.spyOn(global, "window", "get") + windowSpy.mockImplementation( + () => + ({ + location: { + origin: "https://window-origin.com", + }, + }) as unknown as Window & typeof globalThis, + ) + const result = enhanceOryConfig(config) + expect(result.sdk.url).toBe("https://window-origin.com") + windowSpy.mockRestore() + }) + + it("should throw an error if no SDK URL can be determined", () => { + ;(isProduction as jest.Mock).mockReturnValue(false) + delete process.env["NEXT_PUBLIC_ORY_SDK_URL"] + delete process.env["ORY_SDK_URL"] + delete process.env["__NEXT_PRIVATE_ORIGIN"] + delete process.env["VERCEL_URL"] + const config: Partial = {} + expect(() => enhanceOryConfig(config)).toThrow( + "Unable to determine SDK URL. Please set NEXT_PUBLIC_ORY_SDK_URL and/or ORY_SDK_URL or force the SDK URL using `useOryConfig(config, 'https://my-ory-sdk-url.com')`.", + ) + }) +}) diff --git a/packages/nextjs/src/utils/config.ts b/packages/nextjs/src/utils/config.ts new file mode 100644 index 00000000..b390a1d5 --- /dev/null +++ b/packages/nextjs/src/utils/config.ts @@ -0,0 +1,63 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 +import { OryConfig } from "../types" +import { isProduction } from "./sdk" + +/** + * Enhances the Ory config with defaults and SDK URL. The SDK URL is determined as follows: + * + * 1. If `forceSdkUrl` is provided, it is used. + * 2. If `forceSdkUrl` is not provided, the following environment variables are checked: + * - `NEXT_PUBLIC_ORY_SDK_URL` + * - `ORY_SDK_URL` + * - `__NEXT_PRIVATE_ORIGIN` (if not in production) + * - `VERCEL_URL` (if not in production) + * - `window.location.origin` (if not in production) + * - If none of the above are set, an error is thrown. + * + * @param config + * @param forceSdkUrl + */ +export function enhanceOryConfig( + config: Partial, + forceSdkUrl?: string, +) { + let sdkUrl = + forceSdkUrl ?? + process.env["NEXT_PUBLIC_ORY_SDK_URL"] ?? + process.env["ORY_SDK_URL"] + + if (!forceSdkUrl && !isProduction()) { + if (process.env["__NEXT_PRIVATE_ORIGIN"]) { + sdkUrl = process.env["__NEXT_PRIVATE_ORIGIN"].replace(/\/$/, "") + } else if (process.env["VERCEL_URL"]) { + sdkUrl = `https://${process.env["VERCEL_URL"]}`.replace(/\/$/, "") + } else if (typeof window !== "undefined") { + sdkUrl = window.location.origin + } + } + + if (!sdkUrl) { + throw new Error( + "Unable to determine SDK URL. Please set NEXT_PUBLIC_ORY_SDK_URL and/or ORY_SDK_URL or force the SDK URL using `useOryConfig(config, 'https://my-ory-sdk-url.com')`.", + ) + } + + return { + name: config.override?.applicationName ?? "Default name", + sdk: { + url: sdkUrl, + }, + project: { + registration_enabled: true, + verification_enabled: true, + recovery_enabled: true, + recovery_ui_url: config.override?.recoveryUiPath ?? "/ui/recovery", + registration_ui_url: + config.override?.registrationUiPath ?? "/ui/registration", + verification_ui_url: + config.override?.verificationUiPath ?? "/ui/verification", + login_ui_url: config.override?.loginUiPath ?? "/ui/login", + }, + } +} diff --git a/packages/nextjs/src/utils/cookie.test.ts b/packages/nextjs/src/utils/cookie.test.ts new file mode 100644 index 00000000..1cd4d24e --- /dev/null +++ b/packages/nextjs/src/utils/cookie.test.ts @@ -0,0 +1,69 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { guessCookieDomain } from "./cookie" + +describe("cookie guesser", () => { + test("uses force domain", async () => { + expect( + guessCookieDomain("https://localhost", { + forceCookieDomain: "some-domain", + }), + ).toEqual("some-domain") + }) + + test("does not use any guessing domain", async () => { + expect(guessCookieDomain("https://localhost", {})).toEqual("localhost") + }) + + test("is not confused by invalid data", async () => { + expect(guessCookieDomain("https://123.123.123.123.123", {})).toEqual( + undefined, + ) + }) + + test("is not confused by IPv4", async () => { + expect(guessCookieDomain("https://123.123.123.123", {})).toEqual( + "123.123.123.123", + ) + }) + + test("is not confused by IPv6", async () => { + expect( + guessCookieDomain("https://2001:0000:130F:0000:0000:09C0:876A:130B", {}), + ).toEqual(undefined) + }) + + test("uses TLD", async () => { + expect(guessCookieDomain("https://www.example.org", {})).toEqual( + "example.org", + ) + + expect(guessCookieDomain("https://www.example.org:1234", {})).toEqual( + "example.org", + ) + expect(guessCookieDomain("https://localhost/123", {})).toEqual("localhost") + expect(guessCookieDomain("https://foo.localhost/123", {})).toEqual( + "foo.localhost", + ) + expect(guessCookieDomain("https://localhost:1234/123", {})).toEqual( + "localhost", + ) + }) + + test("understands public suffix list", () => { + expect( + guessCookieDomain( + "https://spark-public.s3.amazonaws.com/self-service/login", + {}, + ), + ).toEqual("spark-public.s3.amazonaws.com") + + expect(guessCookieDomain("spark-public.s3.amazonaws.com", {})).toEqual( + "spark-public.s3.amazonaws.com", + ) + expect( + guessCookieDomain("https://docs-gamma-seven.vercel.app", {}), + ).toEqual("docs-gamma-seven.vercel.app") + }) +}) diff --git a/packages/nextjs/src/utils/cookie.ts b/packages/nextjs/src/utils/cookie.ts new file mode 100644 index 00000000..b06387f0 --- /dev/null +++ b/packages/nextjs/src/utils/cookie.ts @@ -0,0 +1,47 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { OryConfig } from "../types" +import { ErrorResult, parse } from "psl" + +function isErrorResult(result: any): result is ErrorResult { + return result && result.error +} + +export function guessCookieDomain(url: string | undefined, config: OryConfig) { + if (!url || config.forceCookieDomain) { + return config.forceCookieDomain + } + + let parsedUrl + try { + parsedUrl = new URL(url).hostname + } catch (e) { + parsedUrl = url + } + + if (isIPAddress(parsedUrl)) { + return parsedUrl + } + + const parsed = parse(parsedUrl) + + if (isErrorResult(parsed)) { + return undefined + } + + return parsed.domain || parsed.input +} + +// Helper function to check if the hostname is an IP address +export function isIPAddress(hostname: string) { + // IPv4 pattern: four groups of 1-3 digits, separated by dots, each between 0-255 + const ipv4Pattern = + /^(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})){3}$/ + + // IPv6 pattern: eight groups of 1-4 hexadecimal digits, separated by colons, optional shorthand (::) + const ipv6Pattern = + /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$/ + + return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname) +} diff --git a/packages/nextjs/src/utils/headers.ts b/packages/nextjs/src/utils/headers.ts new file mode 100644 index 00000000..7c5f15e5 --- /dev/null +++ b/packages/nextjs/src/utils/headers.ts @@ -0,0 +1,22 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +export const defaultForwardedHeaders = [ + "accept", + "accept-charset", + "accept-encoding", + "accept-language", + "authorization", + "cache-control", + "content-type", + "cookie", + "host", + "user-agent", + "referer", +] + +export const defaultOmitHeaders = [ + "transfer-encoding", + "content-encoding", + "content-length", +] diff --git a/packages/nextjs/src/utils/rewrite.test.ts b/packages/nextjs/src/utils/rewrite.test.ts new file mode 100644 index 00000000..f8f6eb7c --- /dev/null +++ b/packages/nextjs/src/utils/rewrite.test.ts @@ -0,0 +1,101 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { rewriteUrls, rewriteJsonResponse } from "./rewrite" +import { OryConfig } from "../types" +import { orySdkUrl } from "./sdk" + +jest.mock("./sdk", () => ({ + orySdkUrl: jest.fn(), +})) + +describe("rewriteUrls", () => { + const config: OryConfig = { + override: { + recoveryUiPath: "/custom/recovery", + registrationUiPath: "/custom/registration", + loginUiPath: "/custom/login", + verificationUiPath: "/custom/verification", + settingsUiPath: "/custom/settings", + }, + } + + it("should rewrite URLs based on config overrides", () => { + const source = "https://example.com/ui/login" + const matchBaseUrl = "https://example.com" + const selfUrl = "https://self.com" + const result = rewriteUrls(source, matchBaseUrl, selfUrl, config) + expect(result).toBe("https://self.com/custom/login") + }) + + it("should replace base URL with self URL", () => { + const source = "https://example.com/some/path" + const matchBaseUrl = "https://example.com" + const selfUrl = "https://self.com" + const result = rewriteUrls(source, matchBaseUrl, selfUrl, config) + expect(result).toBe("https://self.com/some/path") + }) +}) + +describe("rewriteJsonResponse", () => { + beforeEach(() => { + ;(orySdkUrl as jest.Mock).mockReturnValue("https://ory-sdk-url.com") + }) + + it("should rewrite URLs in JSON response", () => { + const obj = { + url: "https://ory-sdk-url.com/path", + nested: { + url: "https://ory-sdk-url.com/nested/path", + }, + } + const proxyUrl = "https://proxy-url.com" + const result = rewriteJsonResponse(obj, proxyUrl) + expect(result).toEqual({ + url: "https://proxy-url.com/path", + nested: { + url: "https://proxy-url.com/nested/path", + }, + }) + }) + + it("should remove undefined values from JSON response", () => { + const obj = { + key1: "value1", + key2: undefined, + nested: { + key3: "value3", + key4: undefined, + }, + } + const result = rewriteJsonResponse(obj) + expect(result).toEqual({ + key1: "value1", + nested: { + key3: "value3", + }, + }) + }) + + it("should handle arrays in JSON response", () => { + const obj = { + array: [ + "https://ory-sdk-url.com/item1", + undefined, + { + url: "https://ory-sdk-url.com/item2", + }, + ], + } + const proxyUrl = "https://proxy-url.com" + const result = rewriteJsonResponse(obj, proxyUrl) + expect(result).toEqual({ + array: [ + "https://proxy-url.com/item1", + { + url: "https://proxy-url.com/item2", + }, + ], + }) + }) +}) diff --git a/packages/nextjs/src/utils/rewrite.ts b/packages/nextjs/src/utils/rewrite.ts new file mode 100644 index 00000000..06f3cfa9 --- /dev/null +++ b/packages/nextjs/src/utils/rewrite.ts @@ -0,0 +1,77 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { OryConfig } from "../types" +import { joinUrlPaths } from "./utils" +import { orySdkUrl } from "./sdk" + +export function rewriteUrls( + source: string, + matchBaseUrl: string, + selfUrl: string, + config: OryConfig, +) { + for (const [_, [matchPath, replaceWith]] of [ + // TODO load these dynamically from the project config + ["/ui/recovery", config.override?.recoveryUiPath], + ["/ui/registration", config.override?.registrationUiPath], + ["/ui/login", config.override?.loginUiPath], + ["/ui/verification", config.override?.verificationUiPath], + ["/ui/settings", config.override?.settingsUiPath], + ].entries()) { + const match = joinUrlPaths(matchBaseUrl, matchPath || "") + if (replaceWith && source.startsWith(match)) { + source = source.replaceAll( + match, + new URL(replaceWith, selfUrl).toString(), + ) + } + } + return source.replaceAll( + matchBaseUrl.replace(/\/$/, ""), + new URL(selfUrl).toString().replace(/\/$/, ""), + ) +} + +/** + * Rewrites Ory SDK URLs in JSON responses (objects, arrays, strings) with the provided proxy URL. + * + * If `proxyUrl` is provided, the SDK URL is replaced with the proxy URL. + * + * @param obj + * @param proxyUrl + */ +export function rewriteJsonResponse( + obj: T, + proxyUrl?: string, +): T { + return Object.fromEntries( + Object.entries(obj) + .filter(([_, value]) => value !== undefined) + .map(([key, value]) => { + if (Array.isArray(value)) { + // Recursively process each item in the array + return [ + key, + value + .map((item) => { + if (typeof item === "object" && item !== null) { + return rewriteJsonResponse(item, proxyUrl) + } else if (typeof item === "string" && proxyUrl) { + return item.replaceAll(orySdkUrl(), proxyUrl) + } + return item + }) + .filter((item) => item !== undefined), + ] + } else if (typeof value === "object" && value !== null) { + // Recursively remove undefined in nested objects + return [key, rewriteJsonResponse(value, proxyUrl)] + } else if (typeof value === "string" && proxyUrl) { + // Replace SDK URL with the provided proxy URL + return [key, value.replaceAll(orySdkUrl(), proxyUrl)] + } + return [key, value] + }), + ) as T +} diff --git a/packages/nextjs/src/utils/sdk.test.ts b/packages/nextjs/src/utils/sdk.test.ts new file mode 100644 index 00000000..1f23793a --- /dev/null +++ b/packages/nextjs/src/utils/sdk.test.ts @@ -0,0 +1,107 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// sdk.test.ts + +import { + orySdkUrl, + isProduction, + guessPotentiallyProxiedOrySdkUrl, +} from "./sdk" + +describe("orySdkUrl", () => { + beforeEach(() => { + delete process.env["NEXT_PUBLIC_ORY_SDK_URL"] + }) + + it("should return NEXT_PUBLIC_ORY_SDK_URL without trailing slash", () => { + process.env["NEXT_PUBLIC_ORY_SDK_URL"] = "https://example.com/" + expect(orySdkUrl()).toBe("https://example.com") + }) + + it("should throw error when NEXT_PUBLIC_ORY_SDK_URL is not set", () => { + expect(() => orySdkUrl()).toThrow( + "You need to set environment variable `NEXT_PUBLIC_ORY_SDK_URL` to your Ory Network SDK URL.", + ) + }) +}) + +describe("isProduction", () => { + beforeEach(() => { + delete process.env["VERCEL_ENV"] + delete process.env["NODE_ENV"] + }) + + it("should return true when VERCEL_ENV is production", () => { + process.env["VERCEL_ENV"] = "production" + expect(isProduction()).toBe(true) + }) + + it("should return true when NODE_ENV is production", () => { + process.env["NODE_ENV"] = "production" + expect(isProduction()).toBe(true) + }) + + it("should return false when VERCEL_ENV and NODE_ENV are not production", () => { + process.env["VERCEL_ENV"] = "development" + process.env["NODE_ENV"] = "test" + expect(isProduction()).toBe(false) + }) + + it("should return false when VERCEL_ENV and NODE_ENV are undefined", () => { + expect(isProduction()).toBe(false) + }) +}) + +describe("guessPotentiallyProxiedOrySdkUrl", () => { + beforeEach(() => { + delete process.env["NEXT_PUBLIC_ORY_SDK_URL"] + delete process.env["VERCEL_ENV"] + delete process.env["NODE_ENV"] + delete process.env["VERCEL_URL"] + delete process.env["__NEXT_PRIVATE_ORIGIN"] + }) + + it("should return orySdkUrl when in production", () => { + process.env["NEXT_PUBLIC_ORY_SDK_URL"] = "https://example.com/" + process.env["VERCEL_ENV"] = "production" + expect(guessPotentiallyProxiedOrySdkUrl()).toBe("https://example.com") + }) + + it("should return https://VERCEL_URL when VERCEL_ENV is set and VERCEL_URL is set", () => { + process.env["VERCEL_ENV"] = "preview" + process.env["VERCEL_URL"] = "myapp.vercel.app" + expect(guessPotentiallyProxiedOrySdkUrl()).toBe("https://myapp.vercel.app") + }) + + it("should return __NEXT_PRIVATE_ORIGIN when __NEXT_PRIVATE_ORIGIN is set", () => { + process.env["VERCEL_ENV"] = "preview" + process.env["__NEXT_PRIVATE_ORIGIN"] = "https://private-origin/" + expect(guessPotentiallyProxiedOrySdkUrl()).toBe("https://private-origin") + }) + + it("should return window.location.origin when window is defined", () => { + const originalWindow = global.window + global.window = { location: { origin: "https://window-origin" } } as any + expect(guessPotentiallyProxiedOrySdkUrl()).toBe("https://window-origin") + global.window = originalWindow + }) + + it("should return knownProxiedUrl when provided", () => { + expect( + guessPotentiallyProxiedOrySdkUrl({ + knownProxiedUrl: "https://known-proxied-url", + }), + ).toBe("https://known-proxied-url") + }) + + it("should return orySdkUrl and log warning when unable to determine SDK URL", () => { + process.env["NEXT_PUBLIC_ORY_SDK_URL"] = "https://example.com/" + const consoleWarnMock = jest.spyOn(console, "warn").mockImplementation() + expect(guessPotentiallyProxiedOrySdkUrl()).toBe("https://example.com") + expect(consoleWarnMock).toHaveBeenCalledWith( + 'Unable to determine a suitable SDK URL for setting up the Next.js integration of Ory Elements. Will proceed using default Ory SDK URL "https://example.com". This is likely not what you want for local development and your authentication and login may not work.', + ) + consoleWarnMock.mockRestore() + }) +}) diff --git a/packages/nextjs/src/utils/sdk.ts b/packages/nextjs/src/utils/sdk.ts new file mode 100644 index 00000000..cca62630 --- /dev/null +++ b/packages/nextjs/src/utils/sdk.ts @@ -0,0 +1,69 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +export function orySdkUrl() { + let baseUrl + + if (process.env["NEXT_PUBLIC_ORY_SDK_URL"]) { + baseUrl = process.env["NEXT_PUBLIC_ORY_SDK_URL"] + } + + if (!baseUrl) { + throw new Error( + "You need to set environment variable `NEXT_PUBLIC_ORY_SDK_URL` to your Ory Network SDK URL.", + ) + } + + return baseUrl.replace(/\/$/, "") +} + +export function isProduction() { + return ( + ["production", "prod"].indexOf( + process.env["VERCEL_ENV"] || process.env["NODE_ENV"] || "", + ) > -1 + ) +} + +export function guessPotentiallyProxiedOrySdkUrl(options?: { + knownProxiedUrl?: string +}) { + if (isProduction()) { + // In production, we use the production custom domain + return orySdkUrl() + } + + if (process.env["VERCEL_ENV"]) { + // We are in vercel + + // The domain name of the generated deployment URL. Example: *.vercel.app. The value does not include the protocol scheme https://. + // + // This is only available for preview deployments on Vercel. + if (!isProduction() && process.env["VERCEL_URL"]) { + return `https://${process.env["VERCEL_URL"]}`.replace(/\/$/, "") + } + + // This is sometimes set by the render server. + if (process.env["__NEXT_PRIVATE_ORIGIN"]) { + return process.env["__NEXT_PRIVATE_ORIGIN"].replace(/\/$/, "") + } + } + + // Unable to figure out the SDK URL. Either because we are not using Vercel or because we are on a local machine. + // Let's try to use the window location. + if (typeof window !== "undefined") { + return window.location.origin + } + + if (options?.knownProxiedUrl) { + return options.knownProxiedUrl + } + + // We tried everything. Let's use the SDK URL. + const final = orySdkUrl() + console.warn( + `Unable to determine a suitable SDK URL for setting up the Next.js integration of Ory Elements. Will proceed using default Ory SDK URL "${final}". This is likely not what you want for local development and your authentication and login may not work.`, + ) + + return final +} diff --git a/packages/nextjs/src/utils/utils.test.ts b/packages/nextjs/src/utils/utils.test.ts new file mode 100644 index 00000000..87587ab8 --- /dev/null +++ b/packages/nextjs/src/utils/utils.test.ts @@ -0,0 +1,198 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// utils.test.ts + +import { + onValidationError, + toFlowParams, + processSetCookieHeaders, + filterRequestHeaders, + joinUrlPaths, +} from "./utils" +import { OryConfig, QueryParams } from "../types" + +describe("onValidationError", () => { + it("should return the same value passed to it", () => { + const value = { key: "value" } + expect(onValidationError(value)).toBe(value) + }) +}) + +describe("toFlowParams", () => { + it("should return FlowParams with id, cookie, and return_to", async () => { + const params: QueryParams = { + ["flow"]: "some-flow-id", + ["return_to"]: "https://example.com/return", + } + const getCookieHeader = jest.fn().mockResolvedValue("some-cookie-value") + + const result = await toFlowParams(params, getCookieHeader) + + expect(result).toEqual({ + id: "some-flow-id", + cookie: "some-cookie-value", + return_to: "https://example.com/return", + }) + expect(getCookieHeader).toHaveBeenCalled() + }) +}) + +describe("processSetCookieHeaders", () => { + ;[ + { + name: "should respect forwarded headers", + protocol: "http", + forwardedProtocol: "https", + host: "console.ory.sh", + forwardedHost: "api.console.ory.sh", + headers: new Headers([ + ["Set-Cookie", "sessionid=abc123; Path=/; HttpOnly"], + ]), + }, + { + name: "should respect regular headers", + protocol: "https", + host: "console.ory.sh", + headers: new Headers([ + ["set-cookie", "sessionid=abc123; Path=/; HttpOnly"], + ]), + }, + { + name: "supports insecure", + protocol: "http", + host: "console.ory.sh", + headers: new Headers([ + ["set-cookie", "sessionid=abc123; Path=/; HttpOnly"], + ]), + }, + { + name: "supports multiple cookies comma separated", + protocol: "http", + host: "console.ory.sh", + headers: new Headers([ + [ + "set-cookie", + "sessionid1=abc123; Path=/; HttpOnly, sessionid2=123abc; Path=/abc; HttpOnly", + ], + ]), + }, + { + name: "supports multiple cookies in record", + protocol: "http", + host: "console.ory.sh", + headers: new Headers([ + ["set-cookie", "sessionid1=abc123; Path=/; HttpOnly"], + ["set-cookiE", "sessionid2=123abc; Path=/abc; HttpOnly"], + ]), + }, + ].forEach( + ({ + name, + protocol, + forwardedProtocol, + host, + forwardedHost, + headers, + forceCookieDomain, + }: { + name: string + protocol: string + forwardedProtocol?: string + host: string + forwardedHost?: string + headers: Headers + forceCookieDomain?: string + }) => { + test(name, () => { + const options: OryConfig = { + forceCookieDomain, + } + const requestHeaders = new Headers() + requestHeaders.set("host", host) + if (forwardedProtocol) { + requestHeaders.set("x-forwarded-proto", forwardedProtocol) + } + if (forwardedHost) { + requestHeaders.set("x-forwarded-host", forwardedHost) + } + + const fetchResponse = new Response(null, { + headers, + }) + + const result = processSetCookieHeaders( + protocol, + fetchResponse, + options, + requestHeaders, + ) + + expect(result).toMatchSnapshot() + }) + }, + ) +}) + +describe("filterRequestHeaders", () => { + it("should filter headers based on default and additional headers", () => { + const headers = new Headers() + headers.set("authorization", "Bearer token") + headers.set("content-type", "application/json") + headers.set("cookie", "sessionid=abc123") + headers.set("x-custom-header", "custom-value") + headers.set("x-ignore-header", "custom-value") + + const forwardAdditionalHeaders = ["x-custom-header"] + + const result = filterRequestHeaders(headers, forwardAdditionalHeaders) + + expect(result.get("authorization")).toBe("Bearer token") + expect(result.get("content-type")).toBe("application/json") + expect(result.get("x-custom-header")).toBe("custom-value") + expect(result.has("x-ignore-header")).toBe(false) + }) + + it("should filter headers based on default headers only when additional headers are not provided", () => { + const headers = new Headers() + headers.set("authorization", "Bearer token") + headers.set("content-type", "application/json") + headers.set("cookie", "sessionid=abc123") + headers.set("x-custom-header", "custom-value") + + const result = filterRequestHeaders(headers) + + expect(result.get("authorization")).toBe("Bearer token") + expect(result.get("content-type")).toBe("application/json") + expect(result.has("x-custom-header")).toBe(false) + }) +}) + +describe("joinUrlPaths", () => { + it("should correctly join base URL and relative URL", () => { + const baseUrl = "https://example.com/api" + const relativeUrl = "/v1/resource" + + const result = joinUrlPaths(baseUrl, relativeUrl) + + expect(result).toBe("https://example.com/api/v1/resource") + }) + + it("should handle base URL without trailing slash", () => { + const baseUrl = "https://example.com/" + const relativeUrl = "v1/resource" + + const result = joinUrlPaths(baseUrl, relativeUrl) + + expect(result).toBe("https://example.com/v1/resource") + }) + + it("should handle relative URL with full URL", () => { + const baseUrl = "https://example.com/api" + const relativeUrl = "https://another.com/resource" + + const result = joinUrlPaths(baseUrl, relativeUrl) + + expect(result).toBe("https://another.com/api/resource") + }) +}) diff --git a/packages/nextjs/src/utils/utils.ts b/packages/nextjs/src/utils/utils.ts new file mode 100644 index 00000000..598f2cb5 --- /dev/null +++ b/packages/nextjs/src/utils/utils.ts @@ -0,0 +1,85 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 +import { parse, splitCookiesString } from "set-cookie-parser" +import { serialize, SerializeOptions } from "cookie" + +import { FlowParams, OryConfig, QueryParams } from "../types" +import { guessCookieDomain } from "./cookie" +import { defaultForwardedHeaders } from "./headers" +import { ApiResponse } from "@ory/client-fetch" +import { rewriteJsonResponse } from "./rewrite" + +export function onValidationError(value: T): T { + return value +} + +export async function toFlowParams( + params: QueryParams, + getCookieHeader: () => Promise, +): Promise { + return { + id: params["flow"], + cookie: await getCookieHeader(), + return_to: params["return_to"], + } +} +export function processSetCookieHeaders( + protocol: string, + fetchResponse: Response, + options: OryConfig, + requestHeaders: Headers, +) { + const isTls = + protocol === "https:" || requestHeaders.get("x-forwarded-proto") === "https" + + const forwarded = requestHeaders.get("x-forwarded-host") + const host = forwarded ? forwarded : requestHeaders.get("host") + const domain = guessCookieDomain(host ?? "", options) + + return parse( + splitCookiesString(fetchResponse.headers.get("set-cookie") || ""), + ) + .map((cookie) => ({ + ...cookie, + domain, + secure: isTls, + encode: (v: string) => v, + })) + .map(({ value, name, ...options }) => + serialize(name, value, options as SerializeOptions), + ) +} + +export function filterRequestHeaders( + headers: Headers, + forwardAdditionalHeaders?: string[], +): Headers { + const filteredHeaders = new Headers() + + headers.forEach((value, key) => { + const isValid = + defaultForwardedHeaders.includes(key) || + (forwardAdditionalHeaders ?? []).includes(key) + if (isValid) filteredHeaders.set(key, value) + }) + + return filteredHeaders +} + +export function joinUrlPaths(baseUrl: string, relativeUrl: string): string { + const base = new URL(baseUrl) + const relative = new URL(relativeUrl, baseUrl) + + relative.pathname = + base.pathname.replace(/\/$/, "") + + "/" + + relative.pathname.replace(/^\//, "") + + return new URL(relative.toString(), baseUrl).href +} + +export function toValue(res: ApiResponse): Promise { + // Remove all undefined values from the response (array and object) using lodash: + // Remove all (nested) undefined values from the response using lodash + return res.value().then((v) => rewriteJsonResponse(v)) +} diff --git a/packages/nextjs/tsconfig.json b/packages/nextjs/tsconfig.json new file mode 100644 index 00000000..6c87fc4c --- /dev/null +++ b/packages/nextjs/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "rootDir": "./src" + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/nextjs/tsconfig.lib.json b/packages/nextjs/tsconfig.lib.json new file mode 100644 index 00000000..32a7f2e0 --- /dev/null +++ b/packages/nextjs/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "rootDir": "./src" + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/nextjs/tsconfig.spec.json b/packages/nextjs/tsconfig.spec.json new file mode 100644 index 00000000..b7635bf5 --- /dev/null +++ b/packages/nextjs/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "types": ["jest", "node"], + "rootDir": "./" + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/packages/nextjs/tsup.config.ts b/packages/nextjs/tsup.config.ts new file mode 100644 index 00000000..e8e89e9a --- /dev/null +++ b/packages/nextjs/tsup.config.ts @@ -0,0 +1,58 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { defineConfig, Options } from "tsup" + +const baseExternal = [ + "next", + "@ory/client-fetch", + "cookie", + "set-cookie-parser", + "tldjs", +] + +const reactExternal = [...baseExternal, "react", "react-dom"] + +const baseConfig: Options = { + dts: true, + minify: false, + sourcemap: true, + bundle: true, + format: ["cjs", "esm"], +} + +export default defineConfig([ + { + ...baseConfig, + entry: ["src/index.ts"], + outDir: "dist/", + treeshake: true, + external: [...baseExternal], + esbuildOptions(options) { + options.banner = { + js: '"use client"', + } + }, + }, + { + ...baseConfig, + entry: ["src/middleware/index.ts"], + outDir: "dist/middleware", + treeshake: true, + external: baseExternal, + }, + { + ...baseConfig, + entry: ["src/app/index.ts"], + outDir: "dist/app", + treeshake: true, + external: reactExternal, + }, + { + ...baseConfig, + entry: ["src/pages/index.ts"], + outDir: "dist/pages", + treeshake: true, + external: reactExternal, + }, +])