From f34f04d7eb4a282d7fc3ccca4213356dd3edf7ca Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Tue, 15 Oct 2024 19:59:00 +0200 Subject: [PATCH] chore: synchronize workspaces --- examples/nextjs-app-router/.env | 1 + examples/nextjs-app-router/.eslintrc.json | 3 + examples/nextjs-app-router/.gitignore | 36 ++ examples/nextjs-app-router/README.md | 36 ++ .../nextjs-app-router/app/(ory)/config.ts | 19 + .../nextjs-app-router/app/(ory)/layout.tsx | 3 + examples/nextjs-app-router/app/(ory)/sdk.ts | 11 + examples/nextjs-app-router/next.config.mjs | 4 + examples/nextjs-app-router/package.json | 30 ++ examples/nextjs-app-router/postcss.config.mjs | 8 + .../app/common/default-forwarded-headers.ts | 13 + .../routers/app/common/get-base-url.ts | 26 + .../app/common/get-cookie-domain.test.ts | 71 +++ .../routers/app/common/get-cookie-domain.ts | 27 + .../app/common/process-location-header.ts | 18 + .../nextjs-app-router/routers/app/layout.tsx | 12 + .../nextjs-app-router/routers/app/login.ts | 62 +++ .../nextjs-app-router/routers/app/proxy.ts | 148 ++++++ .../app/type/create-api-handler-options.ts | 48 ++ .../nextjs-app-router/routers/app/utils.ts | 70 +++ examples/nextjs-app-router/tailwind.config.ts | 19 + examples/nextjs-app-router/tsconfig.json | 26 + package-lock.json | 482 +++++++++++++++++- 23 files changed, 1172 insertions(+), 1 deletion(-) create mode 100644 examples/nextjs-app-router/.env create mode 100644 examples/nextjs-app-router/.eslintrc.json create mode 100644 examples/nextjs-app-router/.gitignore create mode 100644 examples/nextjs-app-router/README.md create mode 100644 examples/nextjs-app-router/app/(ory)/config.ts create mode 100644 examples/nextjs-app-router/app/(ory)/layout.tsx create mode 100644 examples/nextjs-app-router/app/(ory)/sdk.ts create mode 100644 examples/nextjs-app-router/next.config.mjs create mode 100644 examples/nextjs-app-router/package.json create mode 100644 examples/nextjs-app-router/postcss.config.mjs create mode 100644 examples/nextjs-app-router/routers/app/common/default-forwarded-headers.ts create mode 100644 examples/nextjs-app-router/routers/app/common/get-base-url.ts create mode 100644 examples/nextjs-app-router/routers/app/common/get-cookie-domain.test.ts create mode 100644 examples/nextjs-app-router/routers/app/common/get-cookie-domain.ts create mode 100644 examples/nextjs-app-router/routers/app/common/process-location-header.ts create mode 100644 examples/nextjs-app-router/routers/app/layout.tsx create mode 100644 examples/nextjs-app-router/routers/app/login.ts create mode 100644 examples/nextjs-app-router/routers/app/proxy.ts create mode 100644 examples/nextjs-app-router/routers/app/type/create-api-handler-options.ts create mode 100644 examples/nextjs-app-router/routers/app/utils.ts create mode 100644 examples/nextjs-app-router/tailwind.config.ts create mode 100644 examples/nextjs-app-router/tsconfig.json diff --git a/examples/nextjs-app-router/.env b/examples/nextjs-app-router/.env new file mode 100644 index 000000000..3f25fd45d --- /dev/null +++ b/examples/nextjs-app-router/.env @@ -0,0 +1 @@ +ORY_SDK_URL=https://playground.projects.oryapis.com diff --git a/examples/nextjs-app-router/.eslintrc.json b/examples/nextjs-app-router/.eslintrc.json new file mode 100644 index 000000000..372241854 --- /dev/null +++ b/examples/nextjs-app-router/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript"] +} diff --git a/examples/nextjs-app-router/.gitignore b/examples/nextjs-app-router/.gitignore new file mode 100644 index 000000000..fd3dbb571 --- /dev/null +++ b/examples/nextjs-app-router/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/nextjs-app-router/README.md b/examples/nextjs-app-router/README.md new file mode 100644 index 000000000..e215bc4cc --- /dev/null +++ b/examples/nextjs-app-router/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/examples/nextjs-app-router/app/(ory)/config.ts b/examples/nextjs-app-router/app/(ory)/config.ts new file mode 100644 index 000000000..818f88205 --- /dev/null +++ b/examples/nextjs-app-router/app/(ory)/config.ts @@ -0,0 +1,19 @@ +import { OryClientConfiguration } from "@ory/elements-react" + +const config: OryClientConfiguration = { + name: "Ory Elements example app", + sdk: { + url: process.env.ORY_SDK_URL || '' + }, + project:{ + registration_enabled: true, + verification_enabled: true, + recovery_enabled: true, + recovery_ui_url: '/recovery', + registration_ui_url: '/registration', + verification_ui_url: '/verification', + login_ui_url: '/login', + } +} + +export default config diff --git a/examples/nextjs-app-router/app/(ory)/layout.tsx b/examples/nextjs-app-router/app/(ory)/layout.tsx new file mode 100644 index 000000000..15b59b749 --- /dev/null +++ b/examples/nextjs-app-router/app/(ory)/layout.tsx @@ -0,0 +1,3 @@ +import AuthLayout from "@/routers/app/layout"; + +export default AuthLayout diff --git a/examples/nextjs-app-router/app/(ory)/sdk.ts b/examples/nextjs-app-router/app/(ory)/sdk.ts new file mode 100644 index 000000000..7e8668e95 --- /dev/null +++ b/examples/nextjs-app-router/app/(ory)/sdk.ts @@ -0,0 +1,11 @@ +import { Configuration, FrontendApi } from "@ory/client-fetch" + +export function newSDK() { + const config = new Configuration({ + headers: { + Accept: "application/json", + }, + basePath: process.env.ORY_SDK_URL, + }) + return new FrontendApi(config) +} diff --git a/examples/nextjs-app-router/next.config.mjs b/examples/nextjs-app-router/next.config.mjs new file mode 100644 index 000000000..4678774e6 --- /dev/null +++ b/examples/nextjs-app-router/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/examples/nextjs-app-router/package.json b/examples/nextjs-app-router/package.json new file mode 100644 index 000000000..4215d23ac --- /dev/null +++ b/examples/nextjs-app-router/package.json @@ -0,0 +1,30 @@ +{ + "name": "nextjs-app-router", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@types/set-cookie-parser": "^2.4.10", + "cookie": "^1.0.1", + "next": "14.2.15", + "react": "^18", + "react-dom": "^18", + "set-cookie-parser": "^2.7.0" + }, + "devDependencies": { + "@ory/client-fetch": "^1.15.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.15", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/examples/nextjs-app-router/postcss.config.mjs b/examples/nextjs-app-router/postcss.config.mjs new file mode 100644 index 000000000..1a69fd2a4 --- /dev/null +++ b/examples/nextjs-app-router/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/examples/nextjs-app-router/routers/app/common/default-forwarded-headers.ts b/examples/nextjs-app-router/routers/app/common/default-forwarded-headers.ts new file mode 100644 index 000000000..f8d7ed9c6 --- /dev/null +++ b/examples/nextjs-app-router/routers/app/common/default-forwarded-headers.ts @@ -0,0 +1,13 @@ +export const defaultForwardedHeaders = [ + "accept", + "accept-charset", + "accept-encoding", + "accept-language", + "authorization", + "cache-control", + "content-type", + "cookie", + "host", + "user-agent", + "referer", +] diff --git a/examples/nextjs-app-router/routers/app/common/get-base-url.ts b/examples/nextjs-app-router/routers/app/common/get-base-url.ts new file mode 100644 index 000000000..4f4c03d9f --- /dev/null +++ b/examples/nextjs-app-router/routers/app/common/get-base-url.ts @@ -0,0 +1,26 @@ +import { type CreateApiHandlerOptions } from "../type/create-api-handler-options" + +export function getBaseUrl(options: CreateApiHandlerOptions) { + let baseUrl = options.fallbackToPlayground + ? "https://playground.projects.oryapis.com/" + : "" + + if (process.env.ORY_SDK_URL) { + baseUrl = process.env.ORY_SDK_URL + } + + if (process.env.ORY_KRATOS_URL) { + baseUrl = process.env.ORY_KRATOS_URL + } + + if (process.env.ORY_SDK_URL && process.env.ORY_KRATOS_URL) { + throw new Error("Only one of ORY_SDK_URL or ORY_KRATOS_URL can be set.") + } + + if (options.apiBaseUrlOverride) { + baseUrl = options.apiBaseUrlOverride + } + + return baseUrl.replace(/\/$/, "") +} +export { CreateApiHandlerOptions } diff --git a/examples/nextjs-app-router/routers/app/common/get-cookie-domain.test.ts b/examples/nextjs-app-router/routers/app/common/get-cookie-domain.test.ts new file mode 100644 index 000000000..4e5d378c6 --- /dev/null +++ b/examples/nextjs-app-router/routers/app/common/get-cookie-domain.test.ts @@ -0,0 +1,71 @@ +import { guessCookieDomain } from "./get-cookie-domain" + +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", { + dontUseTldForCookieDomain: true, + }), + ).toEqual(undefined) + }) + + test("is not confused by invalid data", async () => { + expect( + guessCookieDomain("5qw5tare4g", { + dontUseTldForCookieDomain: true, + }), + ).toEqual(undefined) + expect( + guessCookieDomain("https://123.123.123.123.123", { + dontUseTldForCookieDomain: true, + }), + ).toEqual(undefined) + }) + + test("is not confused by IP", async () => { + expect( + guessCookieDomain("https://123.123.123.123", { + dontUseTldForCookieDomain: true, + }), + ).toEqual(undefined) + expect( + guessCookieDomain("https://2001:0db8:0000:0000:0000:ff00:0042:8329", { + dontUseTldForCookieDomain: true, + }), + ).toEqual(undefined) + }) + + test("uses TLD", async () => { + expect(guessCookieDomain("https://foo.localhost", {})).toEqual( + "foo.localhost", + ) + + expect(guessCookieDomain("https://foo.localhost:1234", {})).toEqual( + "foo.localhost", + ) + + expect( + guessCookieDomain( + "https://spark-public.s3.amazonaws.com/dataanalysis/loansData.csv", + {}, + ), + ).toEqual("spark-public.s3.amazonaws.com") + + expect(guessCookieDomain("spark-public.s3.amazonaws.com", {})).toEqual( + "spark-public.s3.amazonaws.com", + ) + + expect(guessCookieDomain("https://localhost/123", {})).toEqual("localhost") + expect(guessCookieDomain("https://localhost:1234/123", {})).toEqual( + "localhost", + ) + }) +}) diff --git a/examples/nextjs-app-router/routers/app/common/get-cookie-domain.ts b/examples/nextjs-app-router/routers/app/common/get-cookie-domain.ts new file mode 100644 index 000000000..d307121d7 --- /dev/null +++ b/examples/nextjs-app-router/routers/app/common/get-cookie-domain.ts @@ -0,0 +1,27 @@ +import tldjs from "tldjs" +import { CreateApiHandlerOptions } from "./get-base-url" + +export function guessCookieDomain( + url: string | undefined, + options: CreateApiHandlerOptions, +) { + if (!url || options.forceCookieDomain) { + return options.forceCookieDomain + } + + if (options.dontUseTldForCookieDomain) { + return undefined + } + + const parsed = tldjs.parse(url || "") + + if (!parsed.isValid || parsed.isIp) { + return undefined + } + + if (!parsed.domain) { + return parsed.hostname + } + + return parsed.domain +} diff --git a/examples/nextjs-app-router/routers/app/common/process-location-header.ts b/examples/nextjs-app-router/routers/app/common/process-location-header.ts new file mode 100644 index 000000000..f9ddc0cfa --- /dev/null +++ b/examples/nextjs-app-router/routers/app/common/process-location-header.ts @@ -0,0 +1,18 @@ +export function processLocationHeader( + locationHeaderValue: string, + baseUrl: string, +) { + if (locationHeaderValue.startsWith(baseUrl)) { + return locationHeaderValue.replace(baseUrl, "/api/.ory") + } + + if ( + locationHeaderValue.startsWith("/api/kratos/public/") || + locationHeaderValue.startsWith("/self-service/") || + locationHeaderValue.startsWith("/ui/") + ) { + return "/api/.ory" + locationHeaderValue + } + + return locationHeaderValue +} diff --git a/examples/nextjs-app-router/routers/app/layout.tsx b/examples/nextjs-app-router/routers/app/layout.tsx new file mode 100644 index 000000000..e0f2d47b6 --- /dev/null +++ b/examples/nextjs-app-router/routers/app/layout.tsx @@ -0,0 +1,12 @@ +import "@ory/elements-react/theme/styles.css" +import { PropsWithChildren } from "react" + +export default async function AuthLayout({ children }: PropsWithChildren) { + return ( + <> +
+ {children} +
+ + ) +} diff --git a/examples/nextjs-app-router/routers/app/login.ts b/examples/nextjs-app-router/routers/app/login.ts new file mode 100644 index 000000000..8109bf5f1 --- /dev/null +++ b/examples/nextjs-app-router/routers/app/login.ts @@ -0,0 +1,62 @@ +import { + redirectToBrowserEndpoint, + initOverrides, + onRedirect, + onValidationError, + QueryParams, + toFlowParams, + toValue, +} from "./utils" +import {LoginFlow, FlowType, handleFlowError, FrontendApi} from "@ory/client-fetch" + +/** + * 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 { getOrCreateLoginFlow } from "$/elements/frameworks/nextjs/routers/app/login" + * import { Login } from "$/elements/headless/flows/login" + * + * export default async function LoginPage({ searchParams }: PageProps) { + * const flow = await getOrCreateLoginFlow(searchParams) + * + * return ( + * .projects.oryapis.com" }, + * }} + * /> + * ) + * } + * ``` + * + * @param params The query parameters of the request. + * @param client The client to use for the request. + */ +export async function getOrCreateLoginFlow( + params: QueryParams, + client: FrontendApi +): Promise { + const onRestartFlow = () => redirectToBrowserEndpoint(params, FlowType.Login) + if (!params.flow) { + return onRestartFlow() + } + + try { + const resp = await client.getLoginFlowRaw( + toFlowParams(params), + initOverrides, + ) + + return toValue(resp) + } catch (error) { + const errorHandler = handleFlowError({ + onValidationError, + onRestartFlow, + onRedirect, + }) + await errorHandler(error) + return null + } +} diff --git a/examples/nextjs-app-router/routers/app/proxy.ts b/examples/nextjs-app-router/routers/app/proxy.ts new file mode 100644 index 000000000..166b35da6 --- /dev/null +++ b/examples/nextjs-app-router/routers/app/proxy.ts @@ -0,0 +1,148 @@ +import { SerializeOptions as CookieSerializeOptions, serialize } from "cookie" +import { headers } from "next/headers" +import { redirect } from "next/navigation" +import { NextResponse, type NextRequest } from "next/server" +import parse, { splitCookiesString } from "set-cookie-parser" +import { getBaseUrl } from "./common/get-base-url" +import { CreateApiHandlerOptions } from "./type/create-api-handler-options" +import { defaultForwardedHeaders } from "./common/default-forwarded-headers" +import { processLocationHeader } from "./common/process-location-header" +import { guessCookieDomain } from "./common/get-cookie-domain" + +export function filterRequestHeaders( + 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 +} + +function processSetCookieHeader( + protocol: string, + fetchResponse: Response, + options: CreateApiHandlerOptions, +) { + const requestHeaders = headers() + const isTls = + protocol === "https:" || requestHeaders.get("x-forwarded-proto") === "https" + + const secure = + options.forceCookieSecure === undefined ? isTls : options.forceCookieSecure + + 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, + encode: (v: string) => v, + })) + .map(({ value, name, ...options }) => + serialize(name, value, options as CookieSerializeOptions), + ) +} + +export function createApiHandler(options: CreateApiHandlerOptions) { + const baseUrl = getBaseUrl(options) + + const handler = async ( + request: NextRequest, + { params }: { params: { path: string[] } }, + ) => { + + const path = request.nextUrl.pathname + const url = new URL(path, baseUrl) + url.search = request.nextUrl.search + + const requestHeaders = filterRequestHeaders( + options.forwardAdditionalHeaders, + ) + + requestHeaders.set("X-Ory-Base-URL-Rewrite", "false") + requestHeaders.set("Ory-Base-URL-Rewrite", "false") + requestHeaders.set("Ory-No-Custom-Domain-Redirect", "true") + + try { + const response = await fetch(url, { + method: request.method, + headers: requestHeaders, + body: + request.method !== "GET" && request.method !== "HEAD" + ? await request.arrayBuffer() + : null, + redirect: "manual", + }) + + const responseHeaders = new Headers() + for (const [key, value] of response.headers) { + responseHeaders.append(key, value) + } + + responseHeaders.delete("location") + responseHeaders.delete("set-cookie") + if (response.headers.get("set-cookie")) { + const cookies = processSetCookieHeader( + request.nextUrl.protocol, + response, + options, + ) + cookies.forEach((cookie) => { + responseHeaders.append("Set-Cookie", cookie) + }) + } + + if (response.headers.get("location")) { + const location = processLocationHeader( + response.headers.get("location"), + baseUrl, + ) + responseHeaders.set("location", location) + } + + responseHeaders.delete("transfer-encoding") + responseHeaders.delete("content-encoding") + responseHeaders.delete("content-length") + + const buf = Buffer.from(await response.arrayBuffer()) + + try { + return new NextResponse( + buf.toString("utf-8").replace(new RegExp(baseUrl, "g"), "/api/.ory"), + { + status: response.status, + headers: responseHeaders, + }, + ) + } catch (err) { + return new NextResponse(response.body, { + status: response.status, + headers: responseHeaders, + }) + } + } catch (error) { + console.error(error, { + path, + url, + method: request.method, + headers: requestHeaders, + }) + throw error + } + } + + return { + GET: handler, + POST: handler, + } +} diff --git a/examples/nextjs-app-router/routers/app/type/create-api-handler-options.ts b/examples/nextjs-app-router/routers/app/type/create-api-handler-options.ts new file mode 100644 index 000000000..79637d523 --- /dev/null +++ b/examples/nextjs-app-router/routers/app/type/create-api-handler-options.ts @@ -0,0 +1,48 @@ +export interface CreateApiHandlerOptions { + /** + * If set overrides the API Base URL. Usually, this URL + * is taken from the ORY_KRATOS_URL environment variable. + * + * If you don't have a project you can use the playground project SDK URL: + * + * https://playground.projects.oryapis.com + */ + apiBaseUrlOverride?: 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 the cookie will be set on the hosts top-level-domain. If the app + * runs on www.example.org, the cookie domain will be set automatically to example.org. + * + * Set this option to true to disable that behaviour. + */ + dontUseTldForCookieDomain?: boolean + + /** + * If set to true will set the "Secure" flag for all cookies. This might come in handy when you deploy + * not on Vercel. + */ + forceCookieSecure?: boolean + + /** + * If set to true will fallback to the playground if no other value is set for the Ory SDK URL. + */ + fallbackToPlayground?: boolean + + /* + * 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[] +} diff --git a/examples/nextjs-app-router/routers/app/utils.ts b/examples/nextjs-app-router/routers/app/utils.ts new file mode 100644 index 000000000..0007b212f --- /dev/null +++ b/examples/nextjs-app-router/routers/app/utils.ts @@ -0,0 +1,70 @@ +import { headers } from "next/headers" +import { ApiResponse, FlowType, handleFlowError, OnRedirectHandler } from "@ory/client-fetch" +import { redirect, RedirectType } from "next/navigation" + +export type QueryParams = { [key: string]: any } + +/** + * Get the cookie header from the HTTP request. + * + * @returns The cookie header. + */ +export function getCookieHeader() { + return headers().get("cookie") ?? undefined +} + +export const initOverrides: RequestInit = { + cache: "no-cache", +} + +export function toValue(res: ApiResponse) { + return res.value() +} + +export const onRedirect: OnRedirectHandler = (url, external) => { + redirect(url) +} + +export function onValidationError(value: T): T { + return value +} + +export type FlowParams = { + id: string + cookie: string | undefined + return_to: string +} + +export function toFlowParams(params: QueryParams): FlowParams { + return { + id: params.flow, + cookie: getCookieHeader(), + return_to: params.return_to, + } +} + +export function redirectToBrowserEndpoint(params: QueryParams, flowType: FlowType) { + // Take advantage of the fact, that Ory handles the flow creation for us and redirects the user to the default + // return to automatically if they're logged in already. + return redirect( + sdkUrl()+"/self-service/" + + flowType.toString() + + "/browser?" + + new URLSearchParams(params).toString(), + RedirectType.replace, + ) +} + +export const onError = (onRestartFlow: () => void) => (err: any) => + new Promise((resolve) => { + handleFlowError({ + onValidationError: resolve, + // RestartFlow and Redirect both use redirects hence we don't need to resolve here. + onRestartFlow, + onRedirect, + })(err) + }) + +export function sdkUrl() { + return (process.env.ORY_SDK_URL || '').replace(/\/$/, '') +} \ No newline at end of file diff --git a/examples/nextjs-app-router/tailwind.config.ts b/examples/nextjs-app-router/tailwind.config.ts new file mode 100644 index 000000000..d43da912d --- /dev/null +++ b/examples/nextjs-app-router/tailwind.config.ts @@ -0,0 +1,19 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + colors: { + background: "var(--background)", + foreground: "var(--foreground)", + }, + }, + }, + plugins: [], +}; +export default config; diff --git a/examples/nextjs-app-router/tsconfig.json b/examples/nextjs-app-router/tsconfig.json new file mode 100644 index 000000000..e7ff90fd2 --- /dev/null +++ b/examples/nextjs-app-router/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/package-lock.json b/package-lock.json index b473555d6..78be7ce55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -113,6 +113,470 @@ "npm": ">=8.11.0" } }, + "examples/nextjs-app-router": { + "version": "0.1.0", + "dependencies": { + "@types/set-cookie-parser": "^2.4.10", + "cookie": "^1.0.1", + "next": "14.2.15", + "react": "^18", + "react-dom": "^18", + "set-cookie-parser": "^2.7.0" + }, + "devDependencies": { + "@ory/client-fetch": "^1.15.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.15", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } + }, + "examples/nextjs-app-router/node_modules/@next/env": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.15.tgz", + "integrity": "sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==" + }, + "examples/nextjs-app-router/node_modules/@next/eslint-plugin-next": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.15.tgz", + "integrity": "sha512-pKU0iqKRBlFB/ocOI1Ip2CkKePZpYpnw5bEItEkuZ/Nr9FQP1+p7VDWr4VfOdff4i9bFmrOaeaU1bFEyAcxiMQ==", + "dev": true, + "dependencies": { + "glob": "10.3.10" + } + }, + "examples/nextjs-app-router/node_modules/@next/swc-darwin-arm64": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.15.tgz", + "integrity": "sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "examples/nextjs-app-router/node_modules/@next/swc-darwin-x64": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.15.tgz", + "integrity": "sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "examples/nextjs-app-router/node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.15.tgz", + "integrity": "sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "examples/nextjs-app-router/node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.15.tgz", + "integrity": "sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "examples/nextjs-app-router/node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.15.tgz", + "integrity": "sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "examples/nextjs-app-router/node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.15.tgz", + "integrity": "sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "examples/nextjs-app-router/node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.15.tgz", + "integrity": "sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "examples/nextjs-app-router/node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz", + "integrity": "sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "examples/nextjs-app-router/node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.15.tgz", + "integrity": "sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "examples/nextjs-app-router/node_modules/@ory/client-fetch": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/@ory/client-fetch/-/client-fetch-1.15.6.tgz", + "integrity": "sha512-etWhiJC5kF/qbXugAtUHGi+8Z8L/v+dKXzkRA7jy90tlfSTVem+rmwCPtaH3KaLaYWQ4ltAjOh0LEOCetrE2sQ==", + "dev": true + }, + "examples/nextjs-app-router/node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "examples/nextjs-app-router/node_modules/@types/node": { + "version": "20.16.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", + "integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "examples/nextjs-app-router/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "examples/nextjs-app-router/node_modules/cookie": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.1.tgz", + "integrity": "sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==", + "engines": { + "node": ">=18" + } + }, + "examples/nextjs-app-router/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "examples/nextjs-app-router/node_modules/eslint-config-next": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.15.tgz", + "integrity": "sha512-mKg+NC/8a4JKLZRIOBplxXNdStgxy7lzWuedUaCc8tev+Al9mwDUTujQH6W6qXDH9kycWiVo28tADWGvpBsZcQ==", + "dev": true, + "dependencies": { + "@next/eslint-plugin-next": "14.2.15", + "@rushstack/eslint-patch": "^1.3.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "examples/nextjs-app-router/node_modules/eslint-plugin-react": { + "version": "7.37.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz", + "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.19", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "examples/nextjs-app-router/node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "examples/nextjs-app-router/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "examples/nextjs-app-router/node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "examples/nextjs-app-router/node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "examples/nextjs-app-router/node_modules/next": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.15.tgz", + "integrity": "sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==", + "dependencies": { + "@next/env": "14.2.15", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.15", + "@next/swc-darwin-x64": "14.2.15", + "@next/swc-linux-arm64-gnu": "14.2.15", + "@next/swc-linux-arm64-musl": "14.2.15", + "@next/swc-linux-x64-gnu": "14.2.15", + "@next/swc-linux-x64-musl": "14.2.15", + "@next/swc-win32-arm64-msvc": "14.2.15", + "@next/swc-win32-ia32-msvc": "14.2.15", + "@next/swc-win32-x64-msvc": "14.2.15" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "examples/nextjs-app-router/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "examples/nextjs-app-router/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "examples/nextjs-app-router/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "examples/nextjs-app-router/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, "examples/nextjs-spa": { "version": "0.0.0", "dependencies": { @@ -13134,7 +13598,6 @@ "version": "18.16.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.19.tgz", "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", - "dev": true, "license": "MIT" }, "node_modules/@types/node-fetch": { @@ -13260,6 +13723,14 @@ "@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==", + "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", @@ -26505,6 +26976,10 @@ "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "license": "0BSD" }, + "node_modules/nextjs-app-router": { + "resolved": "examples/nextjs-app-router", + "link": true + }, "node_modules/nextjs-spa": { "resolved": "examples/nextjs-spa", "link": true @@ -29635,6 +30110,11 @@ "dev": true, "license": "ISC" }, + "node_modules/set-cookie-parser": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz", + "integrity": "sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ==" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",