From 0e30b56a1864767175de3366547434b54bd28758 Mon Sep 17 00:00:00 2001 From: Brion Date: Wed, 21 Oct 2026 12:12:12 +0530 Subject: [PATCH] Add the Better Auth Next.js integration sample Move it here from thunder-id/javascript-sdks as integrations/better-auth-nextjs. It now depends on the published @thunderid/better-auth release, carries its own lockfile and the Next.js postcss override, is built by the PR builder and can be released through the release workflow. Refs thunder-id/thunderid#5904 Signed-off-by: Brion --- .github/workflows/release.yml | 8 + README.md | 1 + integrations/better-auth-nextjs/.env.example | 15 + integrations/better-auth-nextjs/.gitignore | 3 + integrations/better-auth-nextjs/.stackblitzrc | 4 + integrations/better-auth-nextjs/README.md | 70 ++ .../app/api/auth/[...all]/route.ts | 6 + .../app/components/ConfigNotice.tsx | 114 +++ .../app/components/HeroCtas.tsx | 20 + .../better-auth-nextjs/app/components/Nav.tsx | 64 ++ .../app/components/ThunderMark.tsx | 36 + .../app/components/icons/BetterAuthLogo.tsx | 11 + .../better-auth-nextjs/app/globals.css | 672 +++++++++++++ .../better-auth-nextjs/app/layout.tsx | 37 + .../better-auth-nextjs/app/not-found.tsx | 13 + integrations/better-auth-nextjs/app/page.tsx | 246 +++++ .../better-auth-nextjs/lib/auth-client.ts | 5 + integrations/better-auth-nextjs/lib/auth.ts | 41 + integrations/better-auth-nextjs/next-env.d.ts | 6 + .../better-auth-nextjs/next.config.ts | 5 + integrations/better-auth-nextjs/package.json | 24 + .../better-auth-nextjs/pnpm-lock.yaml | 926 ++++++++++++++++++ .../better-auth-nextjs/pnpm-workspace.yaml | 7 + .../scripts/prepare-dev.cjs | 21 + integrations/better-auth-nextjs/tsconfig.json | 21 + 25 files changed, 2376 insertions(+) create mode 100644 integrations/better-auth-nextjs/.env.example create mode 100644 integrations/better-auth-nextjs/.gitignore create mode 100644 integrations/better-auth-nextjs/.stackblitzrc create mode 100644 integrations/better-auth-nextjs/README.md create mode 100644 integrations/better-auth-nextjs/app/api/auth/[...all]/route.ts create mode 100644 integrations/better-auth-nextjs/app/components/ConfigNotice.tsx create mode 100644 integrations/better-auth-nextjs/app/components/HeroCtas.tsx create mode 100644 integrations/better-auth-nextjs/app/components/Nav.tsx create mode 100644 integrations/better-auth-nextjs/app/components/ThunderMark.tsx create mode 100644 integrations/better-auth-nextjs/app/components/icons/BetterAuthLogo.tsx create mode 100644 integrations/better-auth-nextjs/app/globals.css create mode 100644 integrations/better-auth-nextjs/app/layout.tsx create mode 100644 integrations/better-auth-nextjs/app/not-found.tsx create mode 100644 integrations/better-auth-nextjs/app/page.tsx create mode 100644 integrations/better-auth-nextjs/lib/auth-client.ts create mode 100644 integrations/better-auth-nextjs/lib/auth.ts create mode 100644 integrations/better-auth-nextjs/next-env.d.ts create mode 100644 integrations/better-auth-nextjs/next.config.ts create mode 100644 integrations/better-auth-nextjs/package.json create mode 100644 integrations/better-auth-nextjs/pnpm-lock.yaml create mode 100644 integrations/better-auth-nextjs/pnpm-workspace.yaml create mode 100644 integrations/better-auth-nextjs/scripts/prepare-dev.cjs create mode 100644 integrations/better-auth-nextjs/tsconfig.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a1dde56..e92ee1e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -86,6 +86,12 @@ on: type: boolean default: false + sample_better_auth_nextjs: + description: 'integrations/better-auth-nextjs' + required: false + type: boolean + default: false + sample_flutter_app_auth: description: 'integrations/flutter-app-auth' required: false @@ -132,6 +138,7 @@ jobs: sample_quickstarts_nuxt: ${{ github.event.inputs.sample_quickstarts_nuxt }} sample_quickstarts_react: ${{ github.event.inputs.sample_quickstarts_react }} sample_quickstarts_vue: ${{ github.event.inputs.sample_quickstarts_vue }} + sample_better_auth_nextjs: ${{ github.event.inputs.sample_better_auth_nextjs }} sample_flutter_app_auth: ${{ github.event.inputs.sample_flutter_app_auth }} sample_self_care_account: ${{ github.event.inputs.sample_self_care_account }} run: | @@ -151,6 +158,7 @@ jobs: add sample_quickstarts_nuxt '{"path":"quickstarts/nuxt","emoji":"πŸ’š","title":"ThunderID Nuxt Quickstart Sample"}' add sample_quickstarts_react '{"path":"quickstarts/react","emoji":"βš›οΈ","title":"ThunderID React Quickstart Sample"}' add sample_quickstarts_vue '{"path":"quickstarts/vue","emoji":"🟩","title":"ThunderID Vue Quickstart Sample"}' + add sample_better_auth_nextjs '{"path":"integrations/better-auth-nextjs","emoji":"πŸ”","title":"ThunderID Better Auth Next.js Sample"}' add sample_flutter_app_auth '{"path":"integrations/flutter-app-auth","emoji":"πŸ¦‹","title":"ThunderID Flutter AppAuth Sample"}' add sample_self_care_account '{"path":"self-care/account","emoji":"πŸ‘€","title":"ThunderID Self-Care Account Sample"}' if [ "$samples" = "[]" ]; then diff --git a/README.md b/README.md index 9ab64bb..7c454b0 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Sample apps for ThunderID. Ready-to-run apps that show ThunderID features in act | [`quickstarts`](quickstarts) | [`react`](quickstarts/react) | [![release](https://img.shields.io/github/v/release/thunder-id/samples?filter=sample/quickstarts/react/*)](https://github.com/thunder-id/samples/releases?q=sample%2Fquickstarts%2Freact&expanded=true) | React app using `@thunderid/react` | | [`quickstarts`](quickstarts) | [`vue`](quickstarts/vue) | [![release](https://img.shields.io/github/v/release/thunder-id/samples?filter=sample/quickstarts/vue/*)](https://github.com/thunder-id/samples/releases?q=sample%2Fquickstarts%2Fvue&expanded=true) | Vue app using `@thunderid/vue` | | [`self-care`](self-care) | [`account`](self-care/account) | [![release](https://img.shields.io/github/v/release/thunder-id/samples?filter=sample/self-care/account/*)](https://github.com/thunder-id/samples/releases?q=sample%2Fself-care%2Faccount&expanded=true) | React account app for viewing and editing a profile and changing a password | +| [`integrations`](integrations) | [`better-auth-nextjs`](integrations/better-auth-nextjs) | [![release](https://img.shields.io/github/v/release/thunder-id/samples?filter=sample/integrations/better-auth-nextjs/*)](https://github.com/thunder-id/samples/releases?q=sample%2Fintegrations%2Fbetter-auth-nextjs&expanded=true) | Next.js sign-in with [Better Auth](https://better-auth.com) and the `@thunderid/better-auth` provider helper | | [`integrations`](integrations) | [`flutter-app-auth`](integrations/flutter-app-auth) | [![release](https://img.shields.io/github/v/release/thunder-id/samples?filter=sample/integrations/flutter-app-auth/*)](https://github.com/thunder-id/samples/releases?q=sample%2Fintegrations%2Fflutter-app-auth&expanded=true) | Flutter sign-in with the `flutter_appauth` plugin | ## Contributing diff --git a/integrations/better-auth-nextjs/.env.example b/integrations/better-auth-nextjs/.env.example new file mode 100644 index 0000000..58f5a81 --- /dev/null +++ b/integrations/better-auth-nextjs/.env.example @@ -0,0 +1,15 @@ +# Secret used by Better Auth to sign/encrypt session cookies. Generate locally, not from the console. +BETTER_AUTH_SECRET=generate-with-openssl-rand-base64-32 +# Base URL this app is served from. Used to derive the OAuth callback URL below. +BETTER_AUTH_URL=http://localhost:3000 + +# ThunderID issuer URL, e.g. https://localhost:8090. The OIDC discovery URL is derived from +# this value: {THUNDERID_ISSUER}/.well-known/openid-configuration. +THUNDERID_ISSUER=https://localhost:8090 + +# OAuth 2.0 / OIDC client credentials from the application's Credentials tab in the console. +THUNDERID_CLIENT_ID=your-client-id-here +THUNDERID_CLIENT_SECRET=your-client-secret-here + +# DANGER: Disables ALL TLS verification. Only for local development with self-signed certs. NEVER use in production. +NODE_TLS_REJECT_UNAUTHORIZED=0 diff --git a/integrations/better-auth-nextjs/.gitignore b/integrations/better-auth-nextjs/.gitignore new file mode 100644 index 0000000..304f800 --- /dev/null +++ b/integrations/better-auth-nextjs/.gitignore @@ -0,0 +1,3 @@ +node_modules +.next +.env diff --git a/integrations/better-auth-nextjs/.stackblitzrc b/integrations/better-auth-nextjs/.stackblitzrc new file mode 100644 index 0000000..5f0a200 --- /dev/null +++ b/integrations/better-auth-nextjs/.stackblitzrc @@ -0,0 +1,4 @@ +{ + "installDependencies": false, + "startCommand": "npm run prepare-dev && npm install && npm run dev" +} diff --git a/integrations/better-auth-nextjs/README.md b/integrations/better-auth-nextjs/README.md new file mode 100644 index 0000000..6b60ad3 --- /dev/null +++ b/integrations/better-auth-nextjs/README.md @@ -0,0 +1,70 @@ +# Better Auth + ThunderID Sample + +Open in StackBlitz + +A minimal Next.js 15 App Router application demonstrating [Better Auth](https://better-auth.com)'s +[Generic OAuth plugin](https://better-auth.com/docs/plugins/generic-oauth) configured for ThunderID via the +[`@thunderid/better-auth`](https://github.com/thunder-id/javascript-sdks/tree/main/packages/better-auth) provider helper. + +All OAuth 2.0 / OIDC handling is performed by Better Auth itself β€” this sample has no ThunderID SDK +dependency, and keeps users/sessions in an in-memory store (`better-auth/adapters/memory`) that resets on +every server restart. Swap it for a real [database adapter](https://better-auth.com/docs/adapters) in a real +app. + +## Prerequisites + +- Node.js 18+ +- pnpm +- A ThunderID application with an OAuth 2.0 / OIDC (`authorization_code`) client + +## Getting started + +1. Copy the example environment file: + + ```bash + cp .env.example .env + ``` + +2. Fill in your ThunderID credentials in `.env`: + + ```dotenv + BETTER_AUTH_SECRET= + BETTER_AUTH_URL=http://localhost:3000 + THUNDERID_ISSUER=https://localhost:8090 + THUNDERID_CLIENT_ID= + THUNDERID_CLIENT_SECRET= + ``` + + `THUNDERID_CLIENT_ID` and `THUNDERID_CLIENT_SECRET` come from the application's Credentials tab in the + ThunderID console. + +3. Register the redirect URI on your ThunderID application (see the app's config notice in this sample for + the exact value to use): + + ``` + http://localhost:3000/api/auth/callback/thunderid + ``` + +4. Install dependencies and start the development server: + + ```bash + pnpm install + pnpm dev + ``` + + The app is now running at [http://localhost:3000](http://localhost:3000). Click **Sign in with + ThunderID** to try the flow. + +## How it works + +- [`lib/auth.ts`](./lib/auth.ts) β€” the Better Auth server instance. Registers the `genericOAuth` plugin with + the `thunderid()` helper from `@thunderid/better-auth`, which supplies the ThunderID issuer's discovery URL + and default `openid profile email` scopes. +- [`lib/auth-client.ts`](./lib/auth-client.ts) β€” the Better Auth React client. No client plugin is needed for + ThunderID; sign-in goes through the standard social-provider API. +- [`app/api/auth/[...all]/route.ts`](./app/api/auth/%5B...all%5D/route.ts) β€” the catch-all route handler that + exposes Better Auth's API via `toNextJsHandler`. +- [`app/page.tsx`](./app/page.tsx) β€” signs in with `authClient.signIn.social({provider: 'thunderid'})`, shows + the session via `authClient.useSession()`, and signs out with `authClient.signOut()`. +- [`app/components/ConfigNotice.tsx`](./app/components/ConfigNotice.tsx) β€” shown instead of the app when a + required environment variable is missing, with the exact redirect URI to register on ThunderID. diff --git a/integrations/better-auth-nextjs/app/api/auth/[...all]/route.ts b/integrations/better-auth-nextjs/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..c587fa4 --- /dev/null +++ b/integrations/better-auth-nextjs/app/api/auth/[...all]/route.ts @@ -0,0 +1,6 @@ +import { toNextJsHandler } from 'better-auth/next-js' +import { getAuth } from '../../../../lib/auth' + +export const { GET, POST } = toNextJsHandler({ + handler: (request) => getAuth().handler(request), +}) diff --git a/integrations/better-auth-nextjs/app/components/ConfigNotice.tsx b/integrations/better-auth-nextjs/app/components/ConfigNotice.tsx new file mode 100644 index 0000000..c3699b4 --- /dev/null +++ b/integrations/better-auth-nextjs/app/components/ConfigNotice.tsx @@ -0,0 +1,114 @@ +'use client' +import { useState } from 'react' +import BetterAuthLogo from './icons/BetterAuthLogo' +import ThunderMark from './ThunderMark' + +function MoonIcon() { + return ( + + + + ) +} + +function SunIcon() { + return ( + + + + + + + + ) +} + +export default function ConfigNotice({ missing }: { missing: string[] }) { + const [dark, setDark] = useState(false) + + const toggle = () => { + const next = !dark + setDark(next) + document.documentElement.classList.toggle('dark', next) + } + + return ( +
+ + +
+
+
+ +
+ +
+ + Setup required + +
+ +

Configuration needed

+ +

+ This sample can't reach ThunderID yet. Follow the steps + below, then restart the dev server. +

+ +
+
Step 1 · Set environment variables
+ +
    + {missing.map((key) => ( +
  • {key}
  • + ))} +
+ +

+ Copy .env.example to .env, fill in the + values from your ThunderID application, then run npm run dev again. +

+
+ +
+
Step 2 · Register the callback URL
+ +
+

+ Better Auth redirects the user to ThunderID and back via this route. In the{' '} + ThunderID Console, open this application and go + to Advanced Settings → OAuth2 Configuration, + then add the exact redirect URI below. +

+ +
+
+
Authorized redirect URI
+ http://localhost:3000/api/auth/callback/thunderid +
+
+
+
+ +

+ Need more info? Take a look at the{' '} + + Better Auth Generic OAuth plugin docs. + +

+
+
+
+ ) +} diff --git a/integrations/better-auth-nextjs/app/components/HeroCtas.tsx b/integrations/better-auth-nextjs/app/components/HeroCtas.tsx new file mode 100644 index 0000000..2a27b41 --- /dev/null +++ b/integrations/better-auth-nextjs/app/components/HeroCtas.tsx @@ -0,0 +1,20 @@ +'use client' +import { useState } from 'react' +import { authClient } from '../../lib/auth-client' + +export default function HeroCtas() { + const [isLoading, setIsLoading] = useState(false) + + const signIn = async () => { + setIsLoading(true) + await authClient.signIn.social({ provider: 'thunderid', callbackURL: '/' }) + } + + return ( +
+ +
+ ) +} diff --git a/integrations/better-auth-nextjs/app/components/Nav.tsx b/integrations/better-auth-nextjs/app/components/Nav.tsx new file mode 100644 index 0000000..523474a --- /dev/null +++ b/integrations/better-auth-nextjs/app/components/Nav.tsx @@ -0,0 +1,64 @@ +'use client' +import { useState } from 'react' +import BetterAuthLogo from './icons/BetterAuthLogo' +import { authClient } from '../../lib/auth-client' + + +function MoonIcon() { + return ( + + + + ) +} + +function SunIcon() { + return ( + + + + + + + + ) +} + +export default function Nav() { + const [dark, setDark] = useState(false) + const { data: session } = authClient.useSession() + + const toggle = () => { + const next = !dark + setDark(next) + document.documentElement.classList.toggle('dark', next) + } + + const signIn = () => { + void authClient.signIn.social({ provider: 'thunderid', callbackURL: '/' }) + } + + const signOut = () => { + void authClient.signOut() + } + + return ( + + ) +} diff --git a/integrations/better-auth-nextjs/app/components/ThunderMark.tsx b/integrations/better-auth-nextjs/app/components/ThunderMark.tsx new file mode 100644 index 0000000..d8cf0ab --- /dev/null +++ b/integrations/better-auth-nextjs/app/components/ThunderMark.tsx @@ -0,0 +1,36 @@ +export default function ThunderMark({ + height = 40, + className, + darkMode = false, +}: { + height?: number + className?: string + darkMode?: boolean +}) { + const width = Math.round((height / 257) * 207) + + return ( + + + + + + ) +} diff --git a/integrations/better-auth-nextjs/app/components/icons/BetterAuthLogo.tsx b/integrations/better-auth-nextjs/app/components/icons/BetterAuthLogo.tsx new file mode 100644 index 0000000..cdd0bbb --- /dev/null +++ b/integrations/better-auth-nextjs/app/components/icons/BetterAuthLogo.tsx @@ -0,0 +1,11 @@ +export default function BetterAuthLogo({ size = 16, fill = 'currentColor' }: { size?: number; fill?: string }) { + return ( + + ) +} diff --git a/integrations/better-auth-nextjs/app/globals.css b/integrations/better-auth-nextjs/app/globals.css new file mode 100644 index 0000000..294110f --- /dev/null +++ b/integrations/better-auth-nextjs/app/globals.css @@ -0,0 +1,672 @@ +/* ── Reset ── */ +*, +::before, +::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +/* ── Design tokens ── */ +:root { + --color-bg: #f7f9fc; + --color-card: #ffffff; + --color-border: #dde3ec; + --color-text: #05213f; + --color-text-muted: #5a7085; + --color-muted: #5a7085; + --color-primary: #3688ff; + --color-primary-hover: #1f72f0; + --color-nav-bg: rgba(247, 249, 252, 0.92); + --color-nav-border: #dde3ec; + --shadow-card: 0 1px 3px 0 rgba(5, 33, 63, 0.06), + 0 4px 12px 0 rgba(5, 33, 63, 0.08); + --font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', + sans-serif; + --radius-card: 12px; + --radius-btn: 8px; +} + +/* ── Dark mode ── */ +.dark, +html.dark { + --color-bg: #080f1c; + --color-card: #0f1a2d; + --color-border: rgba(255, 255, 255, 0.09); + --color-text: #E0EAFF; + --color-text-muted: rgba(224, 234, 255, 0.48); + --color-muted: rgba(224, 234, 255, 0.48); + --color-nav-bg: rgba(8, 15, 28, 0.92); +} + +/* ── Base ── */ +html, +body { + height: 100%; +} + +body { + font-family: var(--font-sans); + background: var(--color-bg); + color: var(--color-text); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + line-height: 1.5; +} + +/* ── App shell ── */ +.app { + min-height: 100vh; + display: flex; + flex-direction: column; + background: var(--color-bg); + color: var(--color-text); + transition: background 0.2s, color 0.2s; +} + +/* ── Navigation ── */ +.nav { + position: fixed; + inset-block-start: 0; + inset-inline: 0; + height: 52px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 28px; + background: var(--color-nav-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid var(--color-nav-border); + z-index: 100; + transition: background 0.2s, border-color 0.2s; +} + +.nav-logo { + display: flex; + align-items: center; + gap: 10px; + text-decoration: none; + color: var(--color-text); +} + +.wordmark-name { + font-size: 14px; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--color-text); + line-height: 1; +} + +.nav-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.dark-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + padding: 0; + background: transparent; + border: 1px solid var(--color-border); + border-radius: var(--radius-btn); + color: var(--color-text-muted); + cursor: pointer; + transition: color 0.15s, background 0.15s, border-color 0.15s; + flex-shrink: 0; +} + +.dark-toggle:hover { + color: var(--color-text); + background: var(--color-border); +} + +/* ── Buttons ── */ +button { + font-family: var(--font-sans); + cursor: pointer; + border: none; + border-radius: var(--radius-btn); + font-weight: 600; + transition: background 0.15s, color 0.15s, border-color 0.15s, opacity 0.15s; + line-height: 1; + white-space: nowrap; +} + +button:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +.btn-primary, +.btn-outline { + display: inline-flex; + align-items: center; + justify-content: center; + height: 46px; + padding: 0 24px; + font-size: 15px; + text-decoration: none; +} + +.btn-primary { + background: var(--color-primary); + color: #ffffff; + border: 1.5px solid var(--color-primary); +} + +.btn-primary:hover { + opacity: 0.88; +} + +.btn-primary:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn-outline { + background: transparent; + color: var(--color-primary); + border: 1.5px solid rgba(54, 136, 255, 0.4); +} + +.btn-outline:hover { + background: rgba(54, 136, 255, 0.08); +} + +/* Nav-sized buttons */ +.nav .btn-primary, +.nav .btn-outline { + height: 36px; + padding: 0 16px; + font-size: 14px; +} + +/* ── Hero section ── */ +.hero { + flex: 1; + padding: 96px 32px 80px; + min-height: 100vh; +} + +.hero-inner { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 0; + max-width: 560px; + width: 100%; + margin: 0 auto; +} + +.hero-mark { + display: flex; + justify-content: center; + margin-bottom: 32px; +} + +.hero-badge { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-text-muted); + margin-bottom: 28px; +} + +.hero-badge-line { + display: block; + width: 24px; + height: 1px; + background: var(--color-border); + flex-shrink: 0; +} + +.hero-title { + font-size: 42px; + font-weight: 700; + letter-spacing: -0.05em; + color: var(--color-text); + line-height: 1.1; + margin: 0 0 18px; +} + +.hero-subtitle { + font-size: 16px; + color: var(--color-text-muted); + max-width: 420px; + line-height: 1.8; + letter-spacing: -0.01em; + margin: 0 auto 36px; +} + +.hero-ctas { + display: flex; + gap: 12px; + flex-wrap: wrap; + justify-content: center; + margin-bottom: 52px; +} + +.hero-divider { + width: 100%; + border: none; + border-top: 1px solid var(--color-border); + margin: 4px 0; +} + +/* ── Stats row ── */ +.hero-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + width: 100%; +} + +.hero-stat { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 16px 8px; + position: relative; +} + +.hero-stat + .hero-stat::before { + content: ''; + position: absolute; + left: 0; + top: 20%; + height: 60%; + width: 1px; + background: var(--color-border); +} + +.hero-stat-value { + font-size: 15px; + font-weight: 700; + color: var(--color-text); + letter-spacing: -0.02em; +} + +.hero-stat-label { + font-size: 12px; + color: var(--color-text-muted); +} + +/* ── Config notice ── */ +.config-badge { + color: #e88b3a; +} + +.config-list { + width: 100%; + max-width: 420px; + text-align: left; + list-style: none; + background: var(--color-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-btn); + padding: 0 16px; + margin: 4px 0 28px; +} + +.config-list-item { + padding: 10px 0; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 13px; + color: var(--color-text); +} + +.config-list-item:not(:last-child) { + border-bottom: 1px solid var(--color-border); +} + +.config-hint { + font-size: 14px; + color: var(--color-text-muted); + line-height: 1.7; + max-width: 420px; + margin: 0 auto; +} + +.config-hint code { + background: rgba(54, 136, 255, 0.1); + border-radius: 4px; + padding: 2px 6px; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 13px; +} + +.config-step { + width: 100%; + max-width: 420px; + margin: 0 auto 28px; +} + +.config-step-label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--color-text-muted); + margin-bottom: 10px; + text-align: left; +} + +.config-step .config-list, +.config-step .config-hint { + margin-left: 0; + margin-right: 0; +} + +.config-box { + text-align: left; + background: var(--color-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-card); + padding: 16px; +} + +.config-box-body { + font-size: 13px; + color: var(--color-text-muted); + line-height: 1.6; + margin-bottom: 14px; +} + +.config-value-group { + display: flex; + flex-direction: column; + gap: 12px; +} + +.config-value-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--color-text-muted); + margin-bottom: 4px; +} + +.config-value { + display: block; + overflow-x: auto; + white-space: nowrap; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 12px; + color: var(--color-text); + background: rgba(54, 136, 255, 0.1); + border-radius: 4px; + padding: 8px 10px; +} + +.config-docs-note { + font-size: 13px; + color: var(--color-text-muted); + max-width: 420px; + margin: 0 auto; +} + +.config-docs-note a { + color: var(--color-primary); + font-weight: 600; +} + +/* ── User avatar ── */ +.user-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + background: var(--color-primary); + color: #ffffff; + display: flex; + align-items: center; + justify-content: center; + font-size: 17px; + font-weight: 700; + letter-spacing: 0.02em; + flex-shrink: 0; +} + +/* ── Signed-in home shell ── */ +.home-shell { + min-height: calc(100vh - 52px); + margin-top: 52px; +} + +/* ── Signed-in home page ── */ +.home-main { + width: 100%; + max-width: 900px; + margin: 0 auto; + padding: 52px 28px 80px; +} + +/* Greeting */ +.home-greeting { + display: flex; + align-items: flex-start; + gap: 16px; + margin-bottom: 36px; +} + +.home-avatar { + flex-shrink: 0; +} + +.home-greeting-text { + min-width: 0; +} + +.home-greeting-name { + margin: 0 0 6px; + font-size: 26px; + font-weight: 700; + color: var(--color-text); + letter-spacing: -0.04em; +} + +.home-greeting-meta { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--color-muted); + letter-spacing: -0.01em; + flex-wrap: wrap; +} + +.home-dot { + display: inline-block; + width: 3px; + height: 3px; + border-radius: 50%; + background: var(--color-border); + flex-shrink: 0; +} + +.home-session-active { + display: inline-flex; + align-items: center; + gap: 5px; + color: #2fbd6b; +} + +.home-session-dot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: #2fbd6b; + flex-shrink: 0; +} + +/* Session stats row */ +.home-stats { + display: flex; + gap: 0; + padding: 20px 0; + border-top: 1px solid var(--color-border); + border-bottom: 1px solid var(--color-border); + margin-bottom: 52px; +} + +.home-stat { + flex: 1; + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.home-stat--bordered { + padding-left: 32px; + border-left: 1px solid var(--color-border); +} + +.home-stat-icon { + width: 32px; + height: 32px; + border-radius: 8px; + background: rgba(54, 136, 255, 0.1); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: #3688ff; +} + +.home-stat-value { + font-size: 14px; + font-weight: 700; + color: var(--color-text); + letter-spacing: -0.02em; +} + +.home-stat-value--mono { + font-family: monospace; + letter-spacing: 0.01em; +} + +.home-stat-label { + font-size: 11px; + font-weight: 600; + color: var(--color-muted); + text-transform: uppercase; + letter-spacing: 0.07em; + margin-top: 2px; +} + +/* What's next */ +.home-next-label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--color-muted); + margin-bottom: 20px; +} + +.home-next-list { + display: flex; + flex-direction: column; +} + +.home-next-item { + display: grid; + grid-template-columns: 28px 1fr auto; + gap: 16px; + align-items: start; + padding: 20px 0; + border-top: 1px solid var(--color-border); + text-decoration: none; + color: inherit; + cursor: pointer; + transition: background 0.2s; + border-radius: 4px; +} + +.home-next-item:hover .home-next-cta { + opacity: 1; +} + +.home-next-n { + font-size: 11px; + font-weight: 700; + color: #3688ff; + letter-spacing: 0.05em; + padding-top: 2px; + font-variant-numeric: tabular-nums; +} + +.home-next-body { + min-width: 0; +} + +.home-next-title { + font-size: 14px; + font-weight: 600; + color: var(--color-text); + letter-spacing: -0.02em; + margin-bottom: 4px; +} + +.home-next-desc { + font-size: 13px; + color: var(--color-muted); + line-height: 1.6; + letter-spacing: -0.01em; +} + +.home-next-cta { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 13px; + font-weight: 600; + color: #3688ff; + white-space: nowrap; + opacity: 0.7; + transition: opacity 0.2s; + padding-top: 2px; +} + +/* ── Responsive ── */ +@media (max-width: 480px) { + .nav { + padding: 0 16px; + } + + .hero { + padding: 80px 16px 32px; + } + + .hero-title { + font-size: 2rem; + } + + .hero-stats { + grid-template-columns: 1fr; + } + + .hero-stat + .hero-stat::before { + top: 0; + left: 20%; + width: 60%; + height: 1px; + } + + .hero-ctas { + flex-direction: column; + align-items: stretch; + } +} diff --git a/integrations/better-auth-nextjs/app/layout.tsx b/integrations/better-auth-nextjs/app/layout.tsx new file mode 100644 index 0000000..a53c4ac --- /dev/null +++ b/integrations/better-auth-nextjs/app/layout.tsx @@ -0,0 +1,37 @@ +import type { Metadata } from 'next' +import ConfigNotice from './components/ConfigNotice' +import './globals.css' + +export const dynamic = 'force-dynamic' + +export const metadata: Metadata = { + title: 'Better Auth + ThunderID Sample', + description: 'Signing in to a Better Auth app via the ThunderID generic OAuth provider helper', +} + +const REQUIRED_ENV_VARS = [ + 'BETTER_AUTH_SECRET', + 'THUNDERID_ISSUER', + 'THUNDERID_CLIENT_ID', + 'THUNDERID_CLIENT_SECRET', +] + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + const missingEnvVars = REQUIRED_ENV_VARS.filter((key) => !process.env[key]) + + return ( + + + {missingEnvVars.length > 0 ? ( + + ) : ( + children + )} + + + ) +} diff --git a/integrations/better-auth-nextjs/app/not-found.tsx b/integrations/better-auth-nextjs/app/not-found.tsx new file mode 100644 index 0000000..53bd47d --- /dev/null +++ b/integrations/better-auth-nextjs/app/not-found.tsx @@ -0,0 +1,13 @@ +import Link from 'next/link' + +export default function NotFound() { + return ( +
+
+

Page not found

+

The page you're looking for doesn't exist.

+ Return home +
+
+ ) +} diff --git a/integrations/better-auth-nextjs/app/page.tsx b/integrations/better-auth-nextjs/app/page.tsx new file mode 100644 index 0000000..a7480f6 --- /dev/null +++ b/integrations/better-auth-nextjs/app/page.tsx @@ -0,0 +1,246 @@ +'use client' +import { useEffect, useState } from 'react' +import HeroCtas from './components/HeroCtas' +import Nav from './components/Nav' +import ThunderMark from './components/ThunderMark' +import { authClient } from '../lib/auth-client' + +function greeting(name: string): string { + const h = new Date().getHours() + const tod = h < 12 ? 'morning' : h < 17 ? 'afternoon' : 'evening' + return `Good ${tod}, ${name}.` +} + +function formatTime(date: Date | string | undefined): string { + if (!date) return 'β€”' + return new Date(date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) +} + +function formatCountdown(secsLeft: number): { text: string; color: string } { + if (secsLeft <= 0) return { text: 'Expired', color: '#d95757' } + if (secsLeft < 300) { + const m = Math.floor(secsLeft / 60), s = secsLeft % 60 + return { text: `${m}m ${s}s`, color: '#e88b3a' } + } + if (secsLeft < 3600) { + const m = Math.floor(secsLeft / 60), s = secsLeft % 60 + return { text: `${m}m ${s}s`, color: '#2fbd6b' } + } + const hr = Math.floor(secsLeft / 3600), m = Math.floor((secsLeft % 3600) / 60) + return { text: `${hr}h ${m}m`, color: '#2fbd6b' } +} + +const NEXT_STEPS = [ + { + n: '01', + title: 'Read the provider helper docs', + body: 'See every option the thunderid() helper accepts β€” scopes, PKCE, logout, sign-up gating.', + cta: 'View options', + href: 'https://github.com/thunder-id/javascript-sdks/tree/main/packages/better-auth#options', + }, + { + n: '02', + title: 'Learn the Generic OAuth plugin', + body: 'Understand how Better Auth performs the OAuth 2.0 / OIDC handshake with any provider.', + cta: 'Read guide', + href: 'https://better-auth.com/docs/plugins/generic-oauth', + }, + { + n: '03', + title: 'Swap in a real database', + body: 'This sample keeps users/sessions in memory. Point Better Auth at Postgres, SQLite, or Prisma.', + cta: 'Database adapters', + href: 'https://better-auth.com/docs/concepts/database', + }, + { + n: '04', + title: 'Explore ThunderID', + body: 'Set up an OAuth 2.0 / OIDC application and explore the rest of the ThunderID platform.', + cta: 'ThunderID docs', + href: 'https://thunderid.dev/', + }, +] + +function ClockIcon() { + return ( + + ) +} + +function TimerIcon() { + return ( + + ) +} + +function ProviderIcon() { + return ( + + ) +} + +function ArrowIcon() { + return ( + + ) +} + +function HomeContent({ user, session }: { + user: { name?: string | null; email: string; image?: string | null } + session: { createdAt: Date | string; expiresAt: Date | string } +}) { + const [now, setNow] = useState(Math.floor(Date.now() / 1000)) + + useEffect(() => { + const id = setInterval(() => setNow(Math.floor(Date.now() / 1000)), 1000) + return () => clearInterval(id) + }, []) + + const givenName = user.name?.split(' ')[0] ?? user.email.split('@')[0] + const expiresAt = Math.floor(new Date(session.expiresAt).getTime() / 1000) + const secsLeft = Math.max(0, expiresAt - now) + const countdown = formatCountdown(secsLeft) + const initial = (user.name ?? user.email).charAt(0).toUpperCase() + + return ( +
+
+ {user.image ? ( + + ) : ( +
{initial}
+ )} +
+

{greeting(givenName)}

+
+ {user.email} + + + + Session active + +
+
+
+ +
+
+
+
+
{formatTime(session.createdAt)}
+
Signed in at
+
+
+
+
+
+
+ {countdown.text} +
+
Session expires in
+
+
+
+
+
+
ThunderID
+
OAuth provider
+
+
+
+ +
What's next
+ +
+ ) +} + +export default function HomePage() { + const { data: session, isPending } = authClient.useSession() + + return ( +
+
+ ) +} diff --git a/integrations/better-auth-nextjs/lib/auth-client.ts b/integrations/better-auth-nextjs/lib/auth-client.ts new file mode 100644 index 0000000..5c6d479 --- /dev/null +++ b/integrations/better-auth-nextjs/lib/auth-client.ts @@ -0,0 +1,5 @@ +import { createAuthClient } from 'better-auth/react' + +// No client plugin is required for the ThunderID provider β€” the generic OAuth plugin +// exposes sign-in through Better Auth's standard social-provider API. +export const authClient = createAuthClient() diff --git a/integrations/better-auth-nextjs/lib/auth.ts b/integrations/better-auth-nextjs/lib/auth.ts new file mode 100644 index 0000000..c8c9cb6 --- /dev/null +++ b/integrations/better-auth-nextjs/lib/auth.ts @@ -0,0 +1,41 @@ +import { thunderid } from '@thunderid/better-auth' +import { betterAuth } from 'better-auth' +import { memoryAdapter, type MemoryDB } from 'better-auth/adapters/memory' +import { nextCookies } from 'better-auth/next-js' +import { genericOAuth } from 'better-auth/plugins' + +// In-memory store β€” fine for this sample, but resets on every server restart. +// Swap for a real database adapter (see https://better-auth.com/docs/adapters) in production. +// The memory adapter only auto-creates a table on its first write, but the OAuth callback reads +// "account" before ever writing to it, so every core table needs to exist upfront. +const db: MemoryDB = { user: [], session: [], account: [], verification: [] } + +function createAuth() { + return betterAuth({ + database: memoryAdapter(db), + plugins: [ + genericOAuth({ + config: [ + thunderid({ + clientId: process.env.THUNDERID_CLIENT_ID!, + clientSecret: process.env.THUNDERID_CLIENT_SECRET!, + issuer: process.env.THUNDERID_ISSUER!, + }), + ], + }), + // Must be listed last β€” it only applies cookies set by plugins earlier in this array. + nextCookies(), + ], + }) +} + +let instance: ReturnType | undefined + +// Built lazily, on the first request, rather than at module scope: the ThunderID issuer URL is +// required, and constructing eagerly would throw as soon as this module is imported β€” which +// happens during `next build`'s page-data collection even when nothing ever calls the handler, +// breaking the build for anyone who hasn't configured `.env` yet. +export function getAuth() { + instance ??= createAuth() + return instance +} diff --git a/integrations/better-auth-nextjs/next-env.d.ts b/integrations/better-auth-nextjs/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/integrations/better-auth-nextjs/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/integrations/better-auth-nextjs/next.config.ts b/integrations/better-auth-nextjs/next.config.ts new file mode 100644 index 0000000..e4f5738 --- /dev/null +++ b/integrations/better-auth-nextjs/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = {} + +export default nextConfig diff --git a/integrations/better-auth-nextjs/package.json b/integrations/better-auth-nextjs/package.json new file mode 100644 index 0000000..75c1e46 --- /dev/null +++ b/integrations/better-auth-nextjs/package.json @@ -0,0 +1,24 @@ +{ + "name": "@thunderid/better-auth-nextjs-sample", + "private": true, + "version": "0.0.0", + "scripts": { + "prepare-dev": "node scripts/prepare-dev.cjs", + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@thunderid/better-auth": "0.1.1", + "better-auth": "^1.7.2", + "next": "^15.5.25", + "react": "^19.2.3", + "react-dom": "^19.2.3" + }, + "devDependencies": { + "@types/node": "^24.7.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "typescript": "^5.9.3" + } +} diff --git a/integrations/better-auth-nextjs/pnpm-lock.yaml b/integrations/better-auth-nextjs/pnpm-lock.yaml new file mode 100644 index 0000000..b03f711 --- /dev/null +++ b/integrations/better-auth-nextjs/pnpm-lock.yaml @@ -0,0 +1,926 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + postcss: 8.5.23 + +importers: + + .: + dependencies: + '@thunderid/better-auth': + specifier: 0.1.1 + version: 0.1.1(better-auth@1.7.11(next@15.5.28(@types/node@24.19.2)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)) + better-auth: + specifier: ^1.7.2 + version: 1.7.11(next@15.5.28(@types/node@24.19.2)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + next: + specifier: ^15.5.25 + version: 15.5.28(@types/node@24.19.2)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: + specifier: ^19.2.3 + version: 19.3.1 + react-dom: + specifier: ^19.2.3 + version: 19.3.1(react@19.3.1) + devDependencies: + '@types/node': + specifier: ^24.7.2 + version: 24.19.2 + '@types/react': + specifier: ^19.2.14 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.3.0(@types/react@19.3.0) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + +packages: + + '@better-auth/core@1.7.11': + resolution: {integrity: sha512-HG08OUeiMIGbDE+qEiBGxEvZ3l0PNEKG2PZfC691UZjLpba/GMtWezP/jtHBP9W1YKZpDEjh5cav39+JGgDV8w==} + peerDependencies: + '@better-auth/utils': ^0.4.2 || ^0.5.0 + '@better-fetch/fetch': 1.3.2 + '@opentelemetry/api': ^1.9.0 + better-call: 1.4.2 + jose: ^6.1.0 + kysely: ^0.28.5 || ^0.29.0 + nanostores: ^1.0.1 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + + '@better-auth/drizzle-adapter@1.7.11': + resolution: {integrity: sha512-CkTDxo4MDymXA4vfLKufkOdV+FgroTCqERE1CNp4XNghiIrBWNjM4Ph6J//AoL6aowWcbLdBqUtJM40/eLJFKg==} + peerDependencies: + '@better-auth/core': ^1.7.11 + '@better-auth/utils': ^0.4.2 || ^0.5.0 + drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 + peerDependenciesMeta: + drizzle-orm: + optional: true + + '@better-auth/kysely-adapter@1.7.11': + resolution: {integrity: sha512-KC/XJGRG2Zi9cSzzKbAU6vmzEiP9t/gd41Ct0e0OhFAlF0aKzfv5SuL0LlEzXlw+sLRAvylaLSxjgctzrCeEzg==} + peerDependencies: + '@better-auth/core': ^1.7.11 + '@better-auth/utils': ^0.4.2 || ^0.5.0 + kysely: ^0.28.17 || ^0.29.0 + peerDependenciesMeta: + kysely: + optional: true + + '@better-auth/memory-adapter@1.7.11': + resolution: {integrity: sha512-rLSPKqQpXDwSE013UmgnJ2oTwDrQD95LkPon/TvwPLkCqnoz7wruNKfMsLCmLxaMhH8QEeZvvS+cE4cl3wqSuw==} + peerDependencies: + '@better-auth/core': ^1.7.11 + '@better-auth/utils': ^0.4.2 || ^0.5.0 + + '@better-auth/mongo-adapter@1.7.11': + resolution: {integrity: sha512-7fEFV7L0FGjfJ3CNi6SL1kmUwfANPHf05znzbPhTozCJ3kZSAP1FWHq7+kyI7xh6dOgaBQ/UdlCUKELqkrkAXQ==} + peerDependencies: + '@better-auth/core': ^1.7.11 + '@better-auth/utils': ^0.4.2 || ^0.5.0 + mongodb: ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + mongodb: + optional: true + + '@better-auth/prisma-adapter@1.7.11': + resolution: {integrity: sha512-kjjiyr7H42z25vrTyAPc/Y19eMUZOByw6jrd5mDoAGZssj1Qm3KPCHZFFxFzTjapW8Saq288xPdt5dARf1rQCA==} + peerDependencies: + '@better-auth/core': ^1.7.11 + '@better-auth/utils': ^0.4.2 || ^0.5.0 + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + '@prisma/client': + optional: true + prisma: + optional: true + + '@better-auth/telemetry@1.7.11': + resolution: {integrity: sha512-VJWa9s0f9EIP/zWmHHd9Gv/PO4nwlXq1Xa5nXq5nsl0BrAJPrx/FbYVIPVWaZR4vDt8f0CWtrN4Qxln8lhpSbA==} + peerDependencies: + '@better-auth/core': ^1.7.11 + '@better-auth/utils': ^0.4.2 || ^0.5.0 + '@better-fetch/fetch': 1.3.2 + + '@better-auth/utils@0.5.0': + resolution: {integrity: sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA==} + + '@better-fetch/fetch@1.3.2': + resolution: {integrity: sha512-Gs7n99b5tqUC6cQAPbV0uED3IraHB6xQbHLQ/C3l7ZFafHScOx9pQ+DYmP5blbLFShVWLqxNUlI9wi4xU/X+ow==} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.5': + resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.5': + resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.5': + resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.4': + resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.4': + resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.4': + resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.4': + resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.4': + resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.4': + resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.4': + resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.4': + resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.5': + resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.5': + resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.5': + resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.5': + resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.5': + resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.5': + resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.5': + resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.5': + resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.5': + resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.5': + resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.5': + resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.5': + resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.5': + resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@next/env@15.5.28': + resolution: {integrity: sha512-xxW5EBI8fMPuVTPBW7LL980IRbuDXlbkUAwp9DDhtb4Lnich3nJ13uYpvrX4VE74hWPf6AvHHAMmGCidcHjbfg==} + + '@next/swc-darwin-arm64@15.5.28': + resolution: {integrity: sha512-dPoQYXcfSIqx5o+TOXVXO7kONK5qbtNRDeiTfII0bT3puGIT6VF5e0QwmxNZfEZ5Zety5f3m9P/p4xqwfiLlPg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.5.28': + resolution: {integrity: sha512-RgCJ7buBivTH+6sddqaVm+/v8kwMHlFkobvdt3DfETutws5HrxnToMIWSN9X3MZ3aAi7BbKrGZZWUDQLEycXqw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.5.28': + resolution: {integrity: sha512-OHXQe13olE59aKe++zHavGcJPh5SLHXi6Anret3BBeq1+qhSuOJDNAli/EDDfxs6UTqA4Qddo4uyYry2slyjiQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@15.5.28': + resolution: {integrity: sha512-nTB7bB6vfn4ezCJNYP43ukt2o8L3+WxKF/sTcTxepdXsJh3SM9RhlU7wrZYbH7L4HO5M87dqDTwm8587vAkCiw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@15.5.28': + resolution: {integrity: sha512-WXCfVI32rW3xCusoUX1RpZKtyChWU2Zwe27JUDmM3KrXwcDNc9vV4zEq0GKFAURnuQ/Ouo/XqPvB0yh445XZCg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@15.5.28': + resolution: {integrity: sha512-Miq+NDb/HWsXuxseYxMM6fuEOQXgTziarkKIRpVKCn8W69cEfhQaQr+90pO+u4q2CbJR2wzXmUuCFyZaJs/Nng==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@15.5.28': + resolution: {integrity: sha512-5LR79lemgDlgxvG7MQJ+zHCkVcrUoQuO7CiIvq3I58vNW4goSOE2b+19Z6FujRWMouvni4gvudYFuFuNxolGWA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.5.28': + resolution: {integrity: sha512-GNipvXUYROSTf4PXEyH95FrWImIB5zdaK6uEOg0RP1/X2drnvoMwiBkJ0VWdzX7w3iiBm3y5g2TxkKDGVTOQcg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@noble/ciphers@2.4.0': + resolution: {integrity: sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@2.4.0': + resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==} + engines: {node: '>= 20.19.0'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@thunderid/better-auth@0.1.1': + resolution: {integrity: sha512-A/uFdKc9YAKDgexVIW4V/1h46ytxYHyYHBBDdBf7h8T+E/fT/q8GZjFj7gRwj4VMXvRy1zHErhjXkDHAdawVkA==} + peerDependencies: + better-auth: '>=1.7.0' + + '@types/node@24.19.2': + resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==} + + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} + peerDependencies: + '@types/react': ^19.3.0 + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + better-auth@1.7.11: + resolution: {integrity: sha512-V4OMDdoFvOuAGbOb/mjNzX/iFJoC3hEF1rpL8vdI5eTR24dMJr7AWJuuhr6dE1ib/CBECzegt64ehS3/ngKYpQ==} + peerDependencies: + '@lynx-js/react': '*' + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + '@sveltejs/kit': ^2.0.0 || ^3.0.0 + '@tanstack/react-start': ^1.0.0 + '@tanstack/solid-start': ^1.0.0 + drizzle-kit: '>=0.31.4 || >=1.0.0-beta.1' + drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 + mongodb: ^6.0.0 || ^7.0.0 + mysql2: ^3.0.0 + next: ^14.0.0 || ^15.0.0 || ^16.0.0 + pg: ^8.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + solid-js: ^1.0.0 + svelte: ^4.0.0 || ^5.0.0 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + vue: ^3.0.0 + peerDependenciesMeta: + '@lynx-js/react': + optional: true + '@prisma/client': + optional: true + '@sveltejs/kit': + optional: true + '@tanstack/react-start': + optional: true + '@tanstack/solid-start': + optional: true + drizzle-kit: + optional: true + drizzle-orm: + optional: true + mongodb: + optional: true + mysql2: + optional: true + next: + optional: true + pg: + optional: true + prisma: + optional: true + react: + optional: true + react-dom: + optional: true + solid-js: + optional: true + svelte: + optional: true + vitest: + optional: true + vue: + optional: true + + better-call@1.4.2: + resolution: {integrity: sha512-lOmFCTG1KcE60YWxoff2Wx0Fv7v7drP/fQTDYnhJUCdkA7V6QWXdLPUiMpOQCq7Fd0Ts3AkrRwD23xoiootlGA==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + caniuse-lite@1.0.30001817: + resolution: {integrity: sha512-uZ1JG80BwhtMalIAGPxPfmNhHyd/B/nTdr3hITWlRamSyAdUStP1Gop01e7SD5q6J90ixJvWDbvmiFVfDgn4bw==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + jose@6.2.13: + resolution: {integrity: sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==} + + kysely@0.29.7: + resolution: {integrity: sha512-fmIq+7kv4WbFm2tcfznRzikqB4cuNpgZ40PSU2fAJ2/OO5XhojqnTPrmgrCjYSRJVwBln03XcRXJGnw9H099IQ==} + engines: {node: '>=22.0.0'} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanostores@1.5.5: + resolution: {integrity: sha512-FixtE239Gl6Gz0ZGCK0WzvNwObNCIwVT+L7zQcf1QgcA6AzBxblzqe9OT88qiucNkhsjSOqiogSaF5jVbJiqSA==} + engines: {node: ^20.0.0 || >=22.0.0} + + next@15.5.28: + resolution: {integrity: sha512-JjhLBBY1PNnuwPtcZ7t8yxVeJVYvD2Jyp633J+gRCNbzSt7N0E+xqCf6uQiff3+3qmUkwcF/ksfSsK1xsZ76Tw==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + + react-dom@19.3.1: + resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==} + peerDependencies: + react: ^19.3.1 + + react@19.3.1: + resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==} + engines: {node: '>=0.10.0'} + + rou3@0.9.2: + resolution: {integrity: sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@3.1.4: + resolution: {integrity: sha512-iEmdEew+Tn1BwMGMP8LzGBEPC7Mf1sySjglCeEPb/Vchm9bnGezePvacYcId+fGprQzIng+TBkzqfJ/qS7AsBg==} + + sharp@0.35.5: + resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} + +snapshots: + + '@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5)': + dependencies: + '@better-auth/utils': 0.5.0 + '@better-fetch/fetch': 1.3.2 + '@standard-schema/spec': 1.1.0 + better-call: 1.4.2(zod@4.6.7) + jose: 6.2.13 + kysely: 0.29.7 + nanostores: 1.5.5 + zod: 4.6.7 + + '@better-auth/drizzle-adapter@1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)': + dependencies: + '@better-auth/core': 1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5) + '@better-auth/utils': 0.5.0 + + '@better-auth/kysely-adapter@1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(kysely@0.29.7)': + dependencies: + '@better-auth/core': 1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5) + '@better-auth/utils': 0.5.0 + optionalDependencies: + kysely: 0.29.7 + + '@better-auth/memory-adapter@1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)': + dependencies: + '@better-auth/core': 1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5) + '@better-auth/utils': 0.5.0 + + '@better-auth/mongo-adapter@1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)': + dependencies: + '@better-auth/core': 1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5) + '@better-auth/utils': 0.5.0 + + '@better-auth/prisma-adapter@1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)': + dependencies: + '@better-auth/core': 1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5) + '@better-auth/utils': 0.5.0 + + '@better-auth/telemetry@1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)': + dependencies: + '@better-auth/core': 1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5) + '@better-auth/utils': 0.5.0 + '@better-fetch/fetch': 1.3.2 + + '@better-auth/utils@0.5.0': + dependencies: + '@noble/hashes': 2.4.0 + + '@better-fetch/fetch@1.3.2': {} + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.4 + optional: true + + '@img/sharp-darwin-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.4 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + optional: true + + '@img/sharp-linux-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.4 + optional: true + + '@img/sharp-linux-arm@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.4 + optional: true + + '@img/sharp-linux-ppc64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.4 + optional: true + + '@img/sharp-linux-riscv64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.4 + optional: true + + '@img/sharp-linux-s390x@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.4 + optional: true + + '@img/sharp-linux-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + optional: true + + '@img/sharp-wasm32@0.35.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-win32-arm64@0.35.5': + optional: true + + '@img/sharp-win32-ia32@0.35.5': + optional: true + + '@img/sharp-win32-x64@0.35.5': + optional: true + + '@next/env@15.5.28': {} + + '@next/swc-darwin-arm64@15.5.28': + optional: true + + '@next/swc-darwin-x64@15.5.28': + optional: true + + '@next/swc-linux-arm64-gnu@15.5.28': + optional: true + + '@next/swc-linux-arm64-musl@15.5.28': + optional: true + + '@next/swc-linux-x64-gnu@15.5.28': + optional: true + + '@next/swc-linux-x64-musl@15.5.28': + optional: true + + '@next/swc-win32-arm64-msvc@15.5.28': + optional: true + + '@next/swc-win32-x64-msvc@15.5.28': + optional: true + + '@noble/ciphers@2.4.0': {} + + '@noble/hashes@2.4.0': {} + + '@standard-schema/spec@1.1.0': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@thunderid/better-auth@0.1.1(better-auth@1.7.11(next@15.5.28(@types/node@24.19.2)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(react-dom@19.3.1(react@19.3.1))(react@19.3.1))': + dependencies: + better-auth: 1.7.11(next@15.5.28(@types/node@24.19.2)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + + '@types/node@24.19.2': + dependencies: + undici-types: 7.24.6 + + '@types/react-dom@19.3.0(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + better-auth@1.7.11(next@15.5.28(@types/node@24.19.2)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(react-dom@19.3.1(react@19.3.1))(react@19.3.1): + dependencies: + '@better-auth/core': 1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5) + '@better-auth/drizzle-adapter': 1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0) + '@better-auth/kysely-adapter': 1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(kysely@0.29.7) + '@better-auth/memory-adapter': 1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0) + '@better-auth/mongo-adapter': 1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0) + '@better-auth/prisma-adapter': 1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0) + '@better-auth/telemetry': 1.7.11(@better-auth/core@1.7.11(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2) + '@better-auth/utils': 0.5.0 + '@better-fetch/fetch': 1.3.2 + '@noble/ciphers': 2.4.0 + '@noble/hashes': 2.4.0 + better-call: 1.4.2(zod@4.6.7) + defu: 6.1.7 + jose: 6.2.13 + kysely: 0.29.7 + nanostores: 1.5.5 + zod: 4.6.7 + optionalDependencies: + next: 15.5.28(@types/node@24.19.2)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + transitivePeerDependencies: + - '@opentelemetry/api' + + better-call@1.4.2(zod@4.6.7): + dependencies: + '@better-auth/utils': 0.5.0 + '@better-fetch/fetch': 1.3.2 + rou3: 0.9.2 + set-cookie-parser: 3.1.4 + optionalDependencies: + zod: 4.6.7 + + caniuse-lite@1.0.30001817: {} + + client-only@0.0.1: {} + + csstype@3.2.3: {} + + defu@6.1.7: {} + + detect-libc@2.1.2: + optional: true + + jose@6.2.13: {} + + kysely@0.29.7: {} + + nanoid@3.3.20: {} + + nanostores@1.5.5: {} + + next@15.5.28(@types/node@24.19.2)(react-dom@19.3.1(react@19.3.1))(react@19.3.1): + dependencies: + '@next/env': 15.5.28 + '@swc/helpers': 0.5.15 + caniuse-lite: 1.0.30001817 + postcss: 8.5.23 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + styled-jsx: 5.1.6(react@19.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 15.5.28 + '@next/swc-darwin-x64': 15.5.28 + '@next/swc-linux-arm64-gnu': 15.5.28 + '@next/swc-linux-arm64-musl': 15.5.28 + '@next/swc-linux-x64-gnu': 15.5.28 + '@next/swc-linux-x64-musl': 15.5.28 + '@next/swc-win32-arm64-msvc': 15.5.28 + '@next/swc-win32-x64-msvc': 15.5.28 + sharp: 0.35.5(@types/node@24.19.2) + transitivePeerDependencies: + - '@babel/core' + - '@types/node' + - babel-plugin-macros + + picocolors@1.1.1: {} + + postcss@8.5.23: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + react-dom@19.3.1(react@19.3.1): + dependencies: + react: 19.3.1 + scheduler: 0.28.0 + + react@19.3.1: {} + + rou3@0.9.2: {} + + scheduler@0.28.0: {} + + semver@7.8.5: + optional: true + + set-cookie-parser@3.1.4: {} + + sharp@0.35.5(@types/node@24.19.2): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.5 + '@img/sharp-darwin-x64': 0.35.5 + '@img/sharp-freebsd-wasm32': 0.35.5 + '@img/sharp-libvips-darwin-arm64': 1.3.4 + '@img/sharp-libvips-darwin-x64': 1.3.4 + '@img/sharp-libvips-linux-arm': 1.3.4 + '@img/sharp-libvips-linux-arm64': 1.3.4 + '@img/sharp-libvips-linux-ppc64': 1.3.4 + '@img/sharp-libvips-linux-riscv64': 1.3.4 + '@img/sharp-libvips-linux-s390x': 1.3.4 + '@img/sharp-libvips-linux-x64': 1.3.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + '@img/sharp-linux-arm': 0.35.5 + '@img/sharp-linux-arm64': 0.35.5 + '@img/sharp-linux-ppc64': 0.35.5 + '@img/sharp-linux-riscv64': 0.35.5 + '@img/sharp-linux-s390x': 0.35.5 + '@img/sharp-linux-x64': 0.35.5 + '@img/sharp-linuxmusl-arm64': 0.35.5 + '@img/sharp-linuxmusl-x64': 0.35.5 + '@img/sharp-webcontainers-wasm32': 0.35.5 + '@img/sharp-win32-arm64': 0.35.5 + '@img/sharp-win32-ia32': 0.35.5 + '@img/sharp-win32-x64': 0.35.5 + '@types/node': 24.19.2 + optional: true + + source-map-js@1.2.2: {} + + styled-jsx@5.1.6(react@19.3.1): + dependencies: + client-only: 0.0.1 + react: 19.3.1 + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + undici-types@7.24.6: {} + + zod@4.6.7: {} diff --git a/integrations/better-auth-nextjs/pnpm-workspace.yaml b/integrations/better-auth-nextjs/pnpm-workspace.yaml new file mode 100644 index 0000000..5c74736 --- /dev/null +++ b/integrations/better-auth-nextjs/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +overrides: + # JUSTIFICATION: Fixes GHSA-6g55-p6wh-862q / GHSA-r28c-9q8g-f849 / GHSA-fxqj-rqcc-2cmp (arbitrary + # file read via attacker-controlled `sourceMappingURL`) and GHSA-qx2v-qp2m-jg93 (XSS via unescaped + # `` in stringify output). Transitive via `next`, which still bundles `postcss@8.4.31` + # internally even on the latest 15.5.x release. Keep the override until Next.js bumps its own + # postcss pin (https://github.com/vercel/next.js). + postcss: 8.5.23 diff --git a/integrations/better-auth-nextjs/scripts/prepare-dev.cjs b/integrations/better-auth-nextjs/scripts/prepare-dev.cjs new file mode 100644 index 0000000..71860cf --- /dev/null +++ b/integrations/better-auth-nextjs/scripts/prepare-dev.cjs @@ -0,0 +1,21 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +const fs = require('node:fs'); +const path = require('node:path'); + +const root = path.join(__dirname, '..'); + +const envExample = path.join(root, '.env.example'); +const envTarget = path.join(root, '.env'); + +// A plain `npm run prepare-dev` (e.g. from .stackblitzrc's startCommand) is a safe, +// idempotent copy-if-missing β€” it never overwrites an `.env` that's already there. +if (fs.existsSync(envExample) && !fs.existsSync(envTarget)) { + const envContent = fs + .readFileSync(envExample, 'utf8') + // Blank placeholder values (e.g. `your-client-id-here`, the sample base URL) so the + // copied .env still trips the app's missing-env-var check until real values are filled in. + .replace(/^([A-Z0-9_]+)=(your-\S*|generate-with-\S*|https:\/\/localhost:8090)$/gm, '$1='); + fs.writeFileSync(envTarget, envContent); +} diff --git a/integrations/better-auth-nextjs/tsconfig.json b/integrations/better-auth-nextjs/tsconfig.json new file mode 100644 index 0000000..6420eed --- /dev/null +++ b/integrations/better-auth-nextjs/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2017", + "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"] +}