diff --git a/.github/actions/bump-version/action.yml b/.github/actions/bump-version/action.yml index 37d66c7..2884a17 100644 --- a/.github/actions/bump-version/action.yml +++ b/.github/actions/bump-version/action.yml @@ -2,7 +2,8 @@ name: Bump Sample Version description: > Bumps the version of a sample app in its native version manifest, so samples built on different stacks can share one release flow. Supported manifests - (checked in order): package.json (npm/pnpm/yarn) and pubspec.yaml (Flutter/Dart). + (checked in order): package.json (npm/pnpm/yarn), pubspec.yaml (Flutter/Dart), + build.gradle.kts (Android) and *.xcodeproj/project.pbxproj (iOS). inputs: path: @@ -72,8 +73,37 @@ runs: fi sed -i -E "s/^version:[[:space:]]*[^[:space:]#]+/version: ${VERSION}+${BUILD}/" "${MANIFEST}" + elif [ -f "${SAMPLE_PATH}/build.gradle.kts" ] && grep -q 'versionName = "' "${SAMPLE_PATH}/build.gradle.kts"; then + MANIFEST="${SAMPLE_PATH}/build.gradle.kts" + CURRENT=$(sed -n -E 's/^[[:space:]]*versionName = "([^"]+)".*/\1/p' "${MANIFEST}" | head -1) + CODE=$(sed -n -E 's/^[[:space:]]*versionCode = ([0-9]+).*/\1/p' "${MANIFEST}" | head -1) + if [ -z "${CURRENT}" ] || [ -z "${CODE}" ]; then + echo "Error: no 'versionName'/'versionCode' found in ${MANIFEST}" + exit 1 + fi + + VERSION=$(bump_semver "${CURRENT}") + # versionCode must increase monotonically for Android to accept the upgrade. + sed -i -E "s/^([[:space:]]*versionName = )\"[^\"]+\"/\1\"${VERSION}\"/" "${MANIFEST}" + sed -i -E "s/^([[:space:]]*versionCode = )[0-9]+/\1$((CODE + 1))/" "${MANIFEST}" + + elif PBXPROJ=$(ls "${SAMPLE_PATH}"/*.xcodeproj/project.pbxproj 2>/dev/null | head -1) && [ -n "${PBXPROJ}" ]; then + MANIFEST="${PBXPROJ}" + CURRENT=$(sed -n -E 's/^[[:space:]]*MARKETING_VERSION = ([^;]+);.*/\1/p' "${MANIFEST}" | head -1) + BUILD=$(sed -n -E 's/^[[:space:]]*CURRENT_PROJECT_VERSION = ([0-9]+);.*/\1/p' "${MANIFEST}" | head -1) + if [ -z "${CURRENT}" ] || [ -z "${BUILD}" ]; then + echo "Error: no 'MARKETING_VERSION'/'CURRENT_PROJECT_VERSION' found in ${MANIFEST}" + exit 1 + fi + + VERSION=$(bump_semver "${CURRENT}") + # Every build configuration carries its own copy, so rewrite all of them. The build + # number (CFBundleVersion) must increase monotonically, like the Flutter +N above. + sed -i -E "s/^([[:space:]]*MARKETING_VERSION = )[^;]+;/\1${VERSION};/" "${MANIFEST}" + sed -i -E "s/^([[:space:]]*CURRENT_PROJECT_VERSION = )[0-9]+;/\1$((BUILD + 1));/" "${MANIFEST}" + else - echo "Error: no supported version manifest (package.json, pubspec.yaml) found in ${SAMPLE_PATH}" + echo "Error: no supported version manifest (package.json, pubspec.yaml, build.gradle.kts, project.pbxproj) found in ${SAMPLE_PATH}" exit 1 fi diff --git a/.github/workflows/pr-builder.yml b/.github/workflows/pr-builder.yml index 3c45108..3a6fcbf 100644 --- a/.github/workflows/pr-builder.yml +++ b/.github/workflows/pr-builder.yml @@ -34,6 +34,15 @@ jobs: with: fail-on-severity: high deny-licenses: GPL-3.0-only,GPL-3.0-or-later,AGPL-3.0-only,AGPL-3.0-or-later + # No patched release exists for either advisory, and both are reached only through nuxt's + # build and dev tooling in quickstarts/nuxt, which ignores them for `pnpm audit` too (see its + # pnpm-workspace.yaml for the full justification). Drop each once upstream ships a fix. + # GHSA-vfj7-8cjw-p6xm: braces stack-exhaustion DoS on the project's own glob patterns. + # GHSA-86w9-cpqp-85rv: node-forge signature verification; only used for local dev certs. + allow-ghsas: GHSA-vfj7-8cjw-p6xm, GHSA-86w9-cpqp-85rv + # ThunderID's own Apache-2.0 package, which the dependency graph reports with the non-SPDX + # lowercase id `apache-2.0`, failing the license validity check. + allow-dependencies-licenses: pkg:pub/thunderid_flutter comment-summary-in-pr: always base-ref: ${{ github.event.pull_request.base.sha }} head-ref: ${{ github.event.pull_request.head.sha }} @@ -43,30 +52,40 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 outputs: - samples: ${{ steps.find.outputs.samples }} + node: ${{ steps.find.outputs.node }} + flutter: ${{ steps.find.outputs.flutter }} + android: ${{ steps.find.outputs.android }} + ios: ${{ steps.find.outputs.ios }} steps: - name: πŸ“₯ Checkout Code uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - # Every // directory at the repo root with a package.json is a sample. + # Every // directory at the repo root is a sample. Its stack is picked from + # the manifest at its root: package.json (Node.js), pubspec.yaml (Flutter), + # settings.gradle.kts (Android) or Package.swift (iOS). - name: πŸ” Find Samples id: find run: | - samples=$(find . -mindepth 3 -maxdepth 3 -name package.json -not -path "./.*" -not -path "*/node_modules/*" \ - -exec dirname {} \; 2>/dev/null | sed 's#^\./##' | sort | jq -R . | jq -cs .) - echo "samples=$samples" >> "$GITHUB_OUTPUT" - echo "Samples: $samples" + find_samples() { + find . -mindepth 3 -maxdepth 3 -name "$1" -not -path "./.*" -not -path "*/node_modules/*" \ + -exec dirname {} \; 2>/dev/null | sed 's#^\./##' | sort | jq -R . | jq -cs . + } + for stack in node:package.json flutter:pubspec.yaml android:settings.gradle.kts ios:Package.swift; do + samples=$(find_samples "${stack#*:}") + echo "${stack%%:*}=$samples" >> "$GITHUB_OUTPUT" + echo "${stack%%:*} samples: $samples" + done build-lint: name: πŸ”¨ Build & Lint (${{ matrix.sample }}) needs: discover - if: ${{ needs.discover.outputs.samples != '[]' }} + if: ${{ needs.discover.outputs.node != '[]' }} runs-on: ubuntu-latest timeout-minutes: 15 strategy: fail-fast: false matrix: - sample: ${{ fromJson(needs.discover.outputs.samples) }} + sample: ${{ fromJson(needs.discover.outputs.node) }} defaults: run: working-directory: ${{ matrix.sample }} @@ -99,11 +118,119 @@ jobs: - name: πŸ” Security Audit run: pnpm audit --audit-level=high + # Not every sample has a formatter, a linter or a build step (e.g. the Node.js + # server quickstarts run their sources directly), so each runs only where defined. - name: 🎨 Check Formatting - run: pnpm format:check + run: pnpm run --if-present format:check - name: 🧹 Lint - run: pnpm lint + run: pnpm run --if-present lint - name: πŸ”¨ Build - run: pnpm build + run: pnpm run --if-present build + + build-flutter: + name: πŸ¦‹ Analyze & Build (${{ matrix.sample }}) + needs: discover + if: ${{ needs.discover.outputs.flutter != '[]' }} + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + sample: ${{ fromJson(needs.discover.outputs.flutter) }} + defaults: + run: + working-directory: ${{ matrix.sample }} + steps: + - name: πŸ“₯ Checkout Code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: β˜• Set up JDK + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: 🐦 Set up Flutter + uses: subosito/flutter-action@4cab68ce0f1c7c924f688bff4792e044f1aeb30c # v2 + with: + channel: stable + cache: true + + # Samples that bundle a .env as an asset need the file present to build. + - name: πŸ” Create .env + run: if [ -f .env.example ] && [ ! -f .env ]; then cp .env.example .env; fi + + - name: πŸ“¦ Get Dependencies + run: flutter pub get + + - name: πŸ” Analyze + run: flutter analyze + + - name: πŸ”¨ Build Android APK + run: flutter build apk --debug + + build-android: + name: πŸ€– Build (${{ matrix.sample }}) + needs: discover + if: ${{ needs.discover.outputs.android != '[]' }} + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + sample: ${{ fromJson(needs.discover.outputs.android) }} + defaults: + run: + working-directory: ${{ matrix.sample }} + steps: + - name: πŸ“₯ Checkout Code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: β˜• Set up JDK + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: πŸ—„οΈ Cache Gradle + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ matrix.sample }}-${{ hashFiles(format('{0}/**/*.gradle.kts', matrix.sample), format('{0}/**/gradle-wrapper.properties', matrix.sample)) }} + restore-keys: | + ${{ runner.os }}-gradle-${{ matrix.sample }}- + + - name: πŸ”¨ Build Debug APK + run: ./gradlew assembleDebug + + build-ios: + name: 🍎 Build (${{ matrix.sample }}) + needs: discover + if: ${{ needs.discover.outputs.ios != '[]' }} + runs-on: macos-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + sample: ${{ fromJson(needs.discover.outputs.ios) }} + defaults: + run: + working-directory: ${{ matrix.sample }} + steps: + - name: πŸ“₯ Checkout Code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: πŸ—„οΈ Cache Swift Packages + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: ${{ matrix.sample }}/.build + key: ${{ runner.os }}-swift-${{ matrix.sample }}-${{ hashFiles(format('{0}/Package.resolved', matrix.sample)) }} + restore-keys: | + ${{ runner.os }}-swift-${{ matrix.sample }}- + + - name: πŸ”¨ Build + run: swift build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef93b29..a1dde56 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,7 @@ # Entrypoint workflow for releasing sample apps. # Each selected sample is released independently: its version is bumped in its -# native manifest (package.json, pubspec.yaml, ...), committed, tagged, packed -# into a source zip and attached to a GitHub Release. +# native manifest (package.json, pubspec.yaml, build.gradle.kts, project.pbxproj), +# committed, tagged, packed into a source zip and attached to a GitHub Release. # Sample jobs run concurrently; push conflicts are handled by a retry-with-rebase # loop in the commit step. @@ -26,6 +26,66 @@ on: type: boolean default: false + sample_quickstarts_android: + description: 'quickstarts/android' + required: false + type: boolean + default: false + + sample_quickstarts_browser: + description: 'quickstarts/browser' + required: false + type: boolean + default: false + + sample_quickstarts_express: + description: 'quickstarts/express' + required: false + type: boolean + default: false + + sample_quickstarts_flutter: + description: 'quickstarts/flutter' + required: false + type: boolean + default: false + + sample_quickstarts_ios: + description: 'quickstarts/ios' + required: false + type: boolean + default: false + + sample_quickstarts_nextjs: + description: 'quickstarts/nextjs' + required: false + type: boolean + default: false + + sample_quickstarts_node: + description: 'quickstarts/node' + required: false + type: boolean + default: false + + sample_quickstarts_nuxt: + description: 'quickstarts/nuxt' + required: false + type: boolean + default: false + + sample_quickstarts_react: + description: 'quickstarts/react' + required: false + type: boolean + default: false + + sample_quickstarts_vue: + description: 'quickstarts/vue' + required: false + type: boolean + default: false + sample_flutter_app_auth: description: 'integrations/flutter-app-auth' required: false @@ -49,6 +109,8 @@ jobs: validate: name: βœ… Validate Inputs runs-on: ubuntu-latest + outputs: + samples: ${{ steps.plan.outputs.samples }} steps: - name: Guard against accidental major bump if: github.event.inputs.bump_type == 'major' && github.event.inputs.confirm_major != 'true' @@ -56,62 +118,58 @@ jobs: echo "❌ bump_type is 'major' but confirm_major is not checked. Enable confirm_major to proceed." exit 1 - # ── Integrations ─────────────────────────────────────────────────────────── - release-flutter-app-auth: - name: πŸ¦‹ Release integrations/flutter-app-auth - needs: [validate] - if: needs.validate.result == 'success' && github.event.inputs.sample_flutter_app_auth == 'true' - runs-on: ubuntu-latest - steps: - - name: πŸ“₯ Checkout Code - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - token: ${{ secrets.THUNDERID_AUTOMATION_BOT }} - - - name: 🏷️ Bump Version - id: bump - uses: ./.github/actions/bump-version - with: - path: integrations/flutter-app-auth - bump_type: ${{ github.event.inputs.bump_type }} - - - name: πŸ“€ Commit, Tag & Push - run: | - git config user.name "${{ env.RELEASE_GIT_USER_NAME }}" - git config user.email "${{ env.RELEASE_GIT_USER_EMAIL }}" - git add "${{ steps.bump.outputs.manifest }}" - git diff --cached --quiet || git commit -m "[Release] integrations/flutter-app-auth@${{ steps.bump.outputs.version }}" - for i in $(seq 1 5); do - git pull --rebase origin ${{ github.ref_name }} - git push origin HEAD:${{ github.ref_name }} && break - [ "$i" -eq 5 ] && exit 1 - sleep $((i * 3)) - done - git tag "sample/integrations/flutter-app-auth/v${{ steps.bump.outputs.version }}" - git push origin "sample/integrations/flutter-app-auth/v${{ steps.bump.outputs.version }}" - - - name: πŸ“¦ Pack Sample - id: pack - uses: ./.github/actions/pack-sample - with: - path: integrations/flutter-app-auth - version: ${{ steps.bump.outputs.version }} - - - name: πŸš€ Create GitHub Release + # Turns the checked sample_* inputs into the release matrix. + - name: πŸ“‹ Plan Releases + id: plan env: - GH_TOKEN: ${{ secrets.THUNDERID_AUTOMATION_BOT }} + sample_quickstarts_android: ${{ github.event.inputs.sample_quickstarts_android }} + sample_quickstarts_browser: ${{ github.event.inputs.sample_quickstarts_browser }} + sample_quickstarts_express: ${{ github.event.inputs.sample_quickstarts_express }} + sample_quickstarts_flutter: ${{ github.event.inputs.sample_quickstarts_flutter }} + sample_quickstarts_ios: ${{ github.event.inputs.sample_quickstarts_ios }} + sample_quickstarts_nextjs: ${{ github.event.inputs.sample_quickstarts_nextjs }} + sample_quickstarts_node: ${{ github.event.inputs.sample_quickstarts_node }} + 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_flutter_app_auth: ${{ github.event.inputs.sample_flutter_app_auth }} + sample_self_care_account: ${{ github.event.inputs.sample_self_care_account }} run: | - gh release create "sample/integrations/flutter-app-auth/v${{ steps.bump.outputs.version }}" \ - --title "ThunderID Flutter AppAuth Sample v${{ steps.bump.outputs.version }}" \ - --generate-notes \ - "${{ steps.pack.outputs.archive }}" - - # ── Self-Care ────────────────────────────────────────────────────────────── - release-self-care-account: - name: πŸ‘€ Release self-care/account + samples='[]' + add() { + if [ "${!1}" = "true" ]; then + samples=$(jq -c --argjson s "$2" '. + [$s]' <<< "$samples") + fi + } + add sample_quickstarts_android '{"path":"quickstarts/android","emoji":"πŸ€–","title":"ThunderID Android Quickstart Sample"}' + add sample_quickstarts_browser '{"path":"quickstarts/browser","emoji":"🌐","title":"ThunderID Browser Quickstart Sample"}' + add sample_quickstarts_express '{"path":"quickstarts/express","emoji":"πŸš‚","title":"ThunderID Express Quickstart Sample"}' + add sample_quickstarts_flutter '{"path":"quickstarts/flutter","emoji":"πŸ¦‹","title":"ThunderID Flutter Quickstart Sample"}' + add sample_quickstarts_ios '{"path":"quickstarts/ios","emoji":"🍎","title":"ThunderID iOS Quickstart Sample"}' + add sample_quickstarts_nextjs '{"path":"quickstarts/nextjs","emoji":"β–²","title":"ThunderID Next.js Quickstart Sample"}' + add sample_quickstarts_node '{"path":"quickstarts/node","emoji":"🟒","title":"ThunderID Node.js Quickstart Sample"}' + 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_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 + echo "❌ No sample selected. Check at least one sample_* input." + exit 1 + fi + echo "samples=$samples" >> "$GITHUB_OUTPUT" + echo "Releasing: $samples" + + release: + name: ${{ matrix.sample.emoji }} Release ${{ matrix.sample.path }} needs: [validate] - if: needs.validate.result == 'success' && github.event.inputs.sample_self_care_account == 'true' runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sample: ${{ fromJson(needs.validate.outputs.samples) }} + env: + SAMPLE_PATH: ${{ matrix.sample.path }} steps: - name: πŸ“₯ Checkout Code uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -122,36 +180,40 @@ jobs: id: bump uses: ./.github/actions/bump-version with: - path: self-care/account + path: ${{ matrix.sample.path }} bump_type: ${{ github.event.inputs.bump_type }} - name: πŸ“€ Commit, Tag & Push + env: + VERSION: ${{ steps.bump.outputs.version }} run: | git config user.name "${{ env.RELEASE_GIT_USER_NAME }}" git config user.email "${{ env.RELEASE_GIT_USER_EMAIL }}" git add "${{ steps.bump.outputs.manifest }}" - git diff --cached --quiet || git commit -m "[Release] self-care/account@${{ steps.bump.outputs.version }}" + git diff --cached --quiet || git commit -m "[Release] ${SAMPLE_PATH}@${VERSION}" for i in $(seq 1 5); do git pull --rebase origin ${{ github.ref_name }} git push origin HEAD:${{ github.ref_name }} && break [ "$i" -eq 5 ] && exit 1 sleep $((i * 3)) done - git tag "sample/self-care/account/v${{ steps.bump.outputs.version }}" - git push origin "sample/self-care/account/v${{ steps.bump.outputs.version }}" + git tag "sample/${SAMPLE_PATH}/v${VERSION}" + git push origin "sample/${SAMPLE_PATH}/v${VERSION}" - name: πŸ“¦ Pack Sample id: pack uses: ./.github/actions/pack-sample with: - path: self-care/account + path: ${{ matrix.sample.path }} version: ${{ steps.bump.outputs.version }} - name: πŸš€ Create GitHub Release env: GH_TOKEN: ${{ secrets.THUNDERID_AUTOMATION_BOT }} + VERSION: ${{ steps.bump.outputs.version }} + TITLE: ${{ matrix.sample.title }} run: | - gh release create "sample/self-care/account/v${{ steps.bump.outputs.version }}" \ - --title "ThunderID Self-Care Account Sample v${{ steps.bump.outputs.version }}" \ + gh release create "sample/${SAMPLE_PATH}/v${VERSION}" \ + --title "${TITLE} v${VERSION}" \ --generate-notes \ "${{ steps.pack.outputs.archive }}" diff --git a/README.md b/README.md index 2b651de..9ab64bb 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,21 @@ ![ThunderID Samples](https://raw.githubusercontent.com/thunder-id/thunderid/refs/heads/main/docs/static/assets/images/readme/repo-banner-samples.png) -Sample apps for ThunderID. Ready-to-run apps that show ThunderID features in action, organized by feature area and released as downloadable source zips. +Sample apps for ThunderID. Ready-to-run apps that show ThunderID features in action, organized by platform or feature area and released as downloadable source zips. Each quickstart uses the published release of its SDK, so it runs on its own. ## Samples | Area | Sample | Release | Description | |---|---|---|---| +| [`quickstarts`](quickstarts) | [`android`](quickstarts/android) | [![release](https://img.shields.io/github/v/release/thunder-id/samples?filter=sample/quickstarts/android/*)](https://github.com/thunder-id/samples/releases?q=sample%2Fquickstarts%2Fandroid&expanded=true) | Android (Jetpack Compose) app using the [ThunderID Android SDK](https://github.com/thunder-id/android-sdks) | +| [`quickstarts`](quickstarts) | [`browser`](quickstarts/browser) | [![release](https://img.shields.io/github/v/release/thunder-id/samples?filter=sample/quickstarts/browser/*)](https://github.com/thunder-id/samples/releases?q=sample%2Fquickstarts%2Fbrowser&expanded=true) | Framework-free JavaScript app using `@thunderid/browser` | +| [`quickstarts`](quickstarts) | [`express`](quickstarts/express) | [![release](https://img.shields.io/github/v/release/thunder-id/samples?filter=sample/quickstarts/express/*)](https://github.com/thunder-id/samples/releases?q=sample%2Fquickstarts%2Fexpress&expanded=true) | Express server app using `@thunderid/express` | +| [`quickstarts`](quickstarts) | [`flutter`](quickstarts/flutter) | [![release](https://img.shields.io/github/v/release/thunder-id/samples?filter=sample/quickstarts/flutter/*)](https://github.com/thunder-id/samples/releases?q=sample%2Fquickstarts%2Fflutter&expanded=true) | Flutter app using the [`thunderid_flutter`](https://pub.dev/packages/thunderid_flutter) package | +| [`quickstarts`](quickstarts) | [`ios`](quickstarts/ios) | [![release](https://img.shields.io/github/v/release/thunder-id/samples?filter=sample/quickstarts/ios/*)](https://github.com/thunder-id/samples/releases?q=sample%2Fquickstarts%2Fios&expanded=true) | iOS (SwiftUI) app using the [ThunderID iOS SDK](https://github.com/thunder-id/ios-sdks) | +| [`quickstarts`](quickstarts) | [`nextjs`](quickstarts/nextjs) | [![release](https://img.shields.io/github/v/release/thunder-id/samples?filter=sample/quickstarts/nextjs/*)](https://github.com/thunder-id/samples/releases?q=sample%2Fquickstarts%2Fnextjs&expanded=true) | Next.js app using `@thunderid/nextjs` | +| [`quickstarts`](quickstarts) | [`node`](quickstarts/node) | [![release](https://img.shields.io/github/v/release/thunder-id/samples?filter=sample/quickstarts/node/*)](https://github.com/thunder-id/samples/releases?q=sample%2Fquickstarts%2Fnode&expanded=true) | Node.js service using `@thunderid/node` for machine-to-machine access | +| [`quickstarts`](quickstarts) | [`nuxt`](quickstarts/nuxt) | [![release](https://img.shields.io/github/v/release/thunder-id/samples?filter=sample/quickstarts/nuxt/*)](https://github.com/thunder-id/samples/releases?q=sample%2Fquickstarts%2Fnuxt&expanded=true) | Nuxt app using `@thunderid/nuxt` | +| [`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) | [`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 | diff --git a/quickstarts/android/.gitignore b/quickstarts/android/.gitignore new file mode 100644 index 0000000..6cba03b --- /dev/null +++ b/quickstarts/android/.gitignore @@ -0,0 +1,42 @@ +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Local build config (copy config.properties.example and fill in your values) +config.properties + +# Local Digital Asset Links file (copy assetlinks.json.example, fill in your values, and +# host it at https:///.well-known/assetlinks.json β€” not served from this app) +assetlinks.json + +# Log/OS Files +*.log +.DS_Store + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.aab +*.apk +output-metadata.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof diff --git a/quickstarts/android/README.md b/quickstarts/android/README.md new file mode 100644 index 0000000..35ca0f8 --- /dev/null +++ b/quickstarts/android/README.md @@ -0,0 +1,64 @@ +# ThunderID Android Quickstart + +ThunderID Android Quickstart demonstrates the full authentication lifecycle using the [ThunderID Android SDK](https://github.com/thunder-id/android-sdks) on Android. + +**Flow demonstrated:** +1. App opens β†’ unauthenticated state (sign-in screen) +2. User initiates sign-in / sign-up β†’ SDK starts app-native Flow Execution +3. User completes the flow +4. Sign-in β†’ authenticated state with profile information, token debugging, and sign-out button. + Sign-up creates the account but does not start a session, so it returns to the landing screen + and the new credentials have to be used to sign in. +5. User taps Sign Out β†’ session terminated, returns to sign-in screen + +The sample depends on the published SDK from [JitPack](https://jitpack.io/#thunder-id/android-sdks) +(`com.github.thunder-id:android-sdks`), declared in `build.gradle.kts`. + +## Prerequisites + +- JDK 17 or later +- Android Studio, or the Android SDK with `ANDROID_HOME` set +- An Android emulator or device (API 26+) +- A running ThunderID instance + +## Get the Sample + +```bash +git clone https://github.com/thunder-id/samples.git +cd samples/quickstarts/android +cp config.properties.example config.properties +``` + +## Configure + +This sample uses app-native authentication (Flow Execution API), so only the base URL and application ID are required, no OAuth2 client ID or redirect URIs. + +| Variable | Description | +|----------|-------------| +| `THUNDERID_BASE_URL` | Base URL of your ThunderID server (HTTPS) | +| `THUNDERID_APPLICATION_ID` | Application UUID from the ThunderID Console | +| `THUNDERID_ATTESTATION_ENABLED` | `true` to send a Google Play Integrity token with each flow (optional) | +| `THUNDERID_CLOUD_PROJECT_NUMBER` | Google Cloud project number used for Play Integrity (optional) | + +`config.properties` is gitignored. Never commit real credentials. Each value can also be supplied as an environment variable of the same name. + +If your ThunderID server runs on `localhost`, don't use `localhost` in `THUNDERID_BASE_URL` directly: + +- **Emulator**: use `https://10.0.2.2:8090`, the emulator's alias for your host machine's loopback. +- **Physical device**: run `adb reverse tcp:8090 tcp:8090` and keep using `https://localhost:8090`, or point the URL at your host machine's LAN IP. + +## Run + +Open this directory in Android Studio, sync Gradle, and run it on an emulator or device. From the command line: + +```bash +./gradlew installDebug +``` + +## Attestation and Passkeys + +See [Try the Android Sample App](https://thunderid.dev/docs/v1.0.x/sdks/android/guides/try-the-sample-app) in the Android SDK docs for Google Play Integrity attestation and passkey (Digital Asset Links, `assetlinks.json.example`) setup. + +## End-to-End Tests + +`thunderid-config/thunderid-config.yaml` declares the application the Android SDK's [E2E suite](https://github.com/thunder-id/android-sdks/tree/main/tests/e2e) provisions on a test server. The suite fetches this sample and builds it against the SDK checkout under test. diff --git a/quickstarts/android/assetlinks.json.example b/quickstarts/android/assetlinks.json.example new file mode 100644 index 0000000..49e55f8 --- /dev/null +++ b/quickstarts/android/assetlinks.json.example @@ -0,0 +1,12 @@ +[ + { + "relation": ["delegate_permission/common.get_login_creds"], + "target": { + "namespace": "android_app", + "package_name": "dev.thunderid.Quickstart", + "sha256_cert_fingerprints": [ + "" + ] + } + } +] diff --git a/quickstarts/android/build.gradle.kts b/quickstarts/android/build.gradle.kts new file mode 100644 index 0000000..0560fcb --- /dev/null +++ b/quickstarts/android/build.gradle.kts @@ -0,0 +1,66 @@ +import java.util.Properties + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "dev.thunderid.quickstart" + compileSdk = 34 + + defaultConfig { + applicationId = "dev.thunderid.Quickstart" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "0.0.0" + + // Load ThunderID config from config.properties (gitignored). + // Copy config.properties.example to config.properties and fill in your values. + val configProps = Properties() + val configFile = rootProject.file("config.properties") + if (configFile.exists()) configFile.inputStream().use { configProps.load(it) } + fun config(key: String) = configProps.getProperty(key) ?: System.getenv(key) ?: "" + val baseUrl = config("THUNDERID_BASE_URL") + val appId = config("THUNDERID_APPLICATION_ID") + val attestationEnabled = config("THUNDERID_ATTESTATION_ENABLED") + val cloudProjectNumber = config("THUNDERID_CLOUD_PROJECT_NUMBER") + buildConfigField("String", "THUNDERID_BASE_URL", "\"$baseUrl\"") + buildConfigField("String", "THUNDERID_APPLICATION_ID", "\"$appId\"") + buildConfigField("boolean", "THUNDERID_ATTESTATION_ENABLED", attestationEnabled.toBoolean().toString()) + buildConfigField("long", "THUNDERID_CLOUD_PROJECT_NUMBER", "${cloudProjectNumber.toLongOrNull() ?: 0L}L") + } + + buildFeatures { + compose = true + buildConfig = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.8" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } +} + +dependencies { + implementation("com.github.thunder-id:android-sdks:v1.4.0") + implementation("com.google.android.play:integrity:1.6.0") + + val composeBom = platform("androidx.compose:compose-bom:2024.02.00") + implementation(composeBom) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.foundation:foundation") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material:material-icons-extended") + implementation("androidx.activity:activity-compose:1.8.2") + implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0") +} diff --git a/quickstarts/android/config.properties.example b/quickstarts/android/config.properties.example new file mode 100644 index 0000000..e605b50 --- /dev/null +++ b/quickstarts/android/config.properties.example @@ -0,0 +1,4 @@ +THUNDERID_BASE_URL=https://10.0.2.2:8090 +THUNDERID_APPLICATION_ID=your-application-id +THUNDERID_ATTESTATION_ENABLED=false +THUNDERID_CLOUD_PROJECT_NUMBER= diff --git a/quickstarts/android/gradle.properties b/quickstarts/android/gradle.properties new file mode 100644 index 0000000..9881f6c --- /dev/null +++ b/quickstarts/android/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX=true +org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError diff --git a/quickstarts/android/gradle/wrapper/gradle-wrapper.jar b/quickstarts/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..13372ae Binary files /dev/null and b/quickstarts/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/quickstarts/android/gradle/wrapper/gradle-wrapper.properties b/quickstarts/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a80b22c --- /dev/null +++ b/quickstarts/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/quickstarts/android/gradlew b/quickstarts/android/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/quickstarts/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/quickstarts/android/settings.gradle.kts b/quickstarts/android/settings.gradle.kts new file mode 100644 index 0000000..060856b --- /dev/null +++ b/quickstarts/android/settings.gradle.kts @@ -0,0 +1,22 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } + plugins { + id("com.android.application") version "8.2.2" + id("org.jetbrains.kotlin.android") version "1.9.22" + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven("https://jitpack.io") + } +} + +rootProject.name = "AndroidQuickstart" diff --git a/quickstarts/android/src/main/AndroidManifest.xml b/quickstarts/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4d70a24 --- /dev/null +++ b/quickstarts/android/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/quickstarts/android/src/main/kotlin/dev/thunderid/quickstart/AuthScreen.kt b/quickstarts/android/src/main/kotlin/dev/thunderid/quickstart/AuthScreen.kt new file mode 100644 index 0000000..831dd3b --- /dev/null +++ b/quickstarts/android/src/main/kotlin/dev/thunderid/quickstart/AuthScreen.kt @@ -0,0 +1,313 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +package dev.thunderid.quickstart + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import dev.thunderid.compose.components.presentation.auth.SignIn +import dev.thunderid.compose.components.presentation.auth.SignUp + +private val PrimaryBlue = Color(0xFF3688FF) +private val BgLight = Color(0xFFF7F9FC) +private val BorderLight = Color(0xFFDDE3EC) +private val TextPrimary = Color(0xFF05213F) +private val TextMuted = Color(0xFF5A7085) + +private val featureTags = listOf("OAuth 2.0", "PKCE", "JWT", "MFA", "SSO") + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AuthScreen(applicationId: String) { + var showSheet by remember { mutableStateOf(null) } + + Box( + modifier = Modifier + .fillMaxSize() + .background(BgLight), + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween, + ) { + // Upper area β€” scrollable + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(Modifier.height(72.dp)) + + // Logo mark + ThunderLogoMark( + modifier = Modifier + .height(80.dp) + .width(65.dp), + darkColor = TextPrimary, + blueColor = PrimaryBlue, + ) + + Spacer(Modifier.height(32.dp)) + + // Headline + Text( + text = "Authentication\nfor developers.", + fontSize = 28.sp, + fontWeight = FontWeight.Bold, + letterSpacing = (-0.5).sp, + color = TextPrimary, + textAlign = TextAlign.Center, + lineHeight = 34.sp, + ) + + Spacer(Modifier.height(12.dp)) + + // Subtext + Text( + text = "OAuth 2.0, PKCE, MFA, and JWT -\nout of the box in minutes.", + fontSize = 15.sp, + color = TextMuted, + textAlign = TextAlign.Center, + lineHeight = 22.sp, + ) + + Spacer(Modifier.height(28.dp)) + + // Feature tags + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + featureTags.forEach { tag -> + Text( + text = tag, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = PrimaryBlue, + modifier = Modifier + .border(1.dp, BorderLight, RoundedCornerShape(20.dp)) + .padding(horizontal = 12.dp, vertical = 6.dp), + ) + } + } + + Spacer(Modifier.height(48.dp)) + } + + // Bottom CTAs + Column( + modifier = Modifier + .padding(horizontal = 24.dp) + .padding(bottom = 40.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Button( + onClick = { showSheet = "signup" }, + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue), + ) { + Text("Get started", fontSize = 16.sp, fontWeight = FontWeight.SemiBold) + } + + OutlinedButton( + onClick = { showSheet = "login" }, + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + shape = RoundedCornerShape(10.dp), + border = androidx.compose.foundation.BorderStroke(1.5.dp, PrimaryBlue), + colors = ButtonDefaults.outlinedButtonColors(contentColor = PrimaryBlue), + ) { + Text("Sign in", fontSize = 16.sp, fontWeight = FontWeight.SemiBold) + } + } + } + } + + // Bottom sheets + if (showSheet != null) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ModalBottomSheet( + onDismissRequest = { showSheet = null }, + sheetState = sheetState, + ) { + when (showSheet) { + "login" -> LoginSheetContent(applicationId = applicationId) + "signup" -> SignUpSheetContent(onComplete = { showSheet = null }) + } + } + } +} + +@Composable +private fun SheetTitle(text: String) { + Text( + text = text, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = TextPrimary, + modifier = Modifier.padding(start = 24.dp, end = 24.dp, bottom = 4.dp), + ) +} + +@Composable +private fun LoginSheetContent(applicationId: String) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(bottom = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + // The flow's own components (fetched by `SignIn`) already include a HEADING_1 + // title β€” adding a sheet title here would duplicate it, unlike SignUp below + // which has no server-rendered heading of its own. + SignIn(applicationId = applicationId, modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)) + } +} + +@Composable +private fun SignUpSheetContent(onComplete: () -> Unit) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(bottom = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SheetTitle("Create account") + Spacer(Modifier.height(8.dp)) + // Registration finishes without establishing a session, so the app stays on AuthScreen and + // this sheet is not torn down with it the way the sign-in sheet is (a successful sign-in + // swaps the whole screen for HomeScreen). Without closing it here the completed flow + // leaves an empty sheet covering the app. + SignUp( + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp), + onComplete = onComplete, + ) + } +} + +@Composable +private fun ThunderLogoMark( + modifier: Modifier = Modifier, + darkColor: Color, + blueColor: Color, +) { + Canvas(modifier = modifier) { + val scaleY = size.height / 257f + val scaleX = size.width / 207f + + fun sx(x: Float) = x * scaleX + fun sy(y: Float) = y * scaleY + + // Path 1 β€” dark fill (top-left quad) + val path1 = Path().apply { + moveTo(sx(55.4763f), sy(26.4391f)) + lineTo(sx(58.8866f), sy(0f)) + lineTo(sx(0f), sy(0f)) + lineTo(sx(0f), sy(26.4391f)) + close() + } + drawPath(path1, darkColor) + + // Path 2 β€” blue left column + val path2 = Path().apply { + moveTo(sx(39.8438f), sy(147.407f)) + lineTo(sx(49.5455f), sy(72.2839f)) + lineTo(sx(0f), sy(72.2839f)) + lineTo(sx(0f), sy(256.743f)) + lineTo(sx(60.5602f), sy(256.743f)) + lineTo(sx(80.048f), sy(147.407f)) + close() + } + drawPath(path2, blueColor) + + // Path 3 β€” blue right arc / body + val path3 = Path().apply { + moveTo(sx(192.42f), sy(59.361f)) + cubicTo( + sx(182.782f), sy(40.2307f), + sx(168.929f), sy(25.5705f), + sx(150.903f), sy(15.3381f), + ) + cubicTo( + sx(145.501f), sy(12.2662f), + sx(139.761f), sy(9.6605f), + sx(133.703f), sy(7.5208f), + ) + lineTo(sx(115.401f), sy(103.702f)) + lineTo(sx(159.757f), sy(103.702f)) + lineTo(sx(76.2987f), sy(256.743f)) + lineTo(sx(83.3735f), sy(256.743f)) + cubicTo( + sx(109.449f), sy(256.743f), + sx(131.69f), sy(251.574f), + sx(150.14f), sy(241.236f), + ) + cubicTo( + sx(168.569f), sy(230.897f), + sx(182.634f), sy(216.131f), + sx(192.356f), sy(196.959f), + ) + cubicTo( + sx(202.058f), sy(177.765f), + sx(206.909f), sy(154.8f), + sx(206.909f), sy(128.043f), + ) + cubicTo( + sx(206.909f), sy(101.286f), + sx(202.079f), sy(78.5123f), + sx(192.441f), sy(59.3821f), + ) + lineTo(sx(192.42f), sy(59.361f)) + close() + } + drawPath(path3, blueColor) + } +} diff --git a/quickstarts/android/src/main/kotlin/dev/thunderid/quickstart/HomeScreen.kt b/quickstarts/android/src/main/kotlin/dev/thunderid/quickstart/HomeScreen.kt new file mode 100644 index 0000000..8190173 --- /dev/null +++ b/quickstarts/android/src/main/kotlin/dev/thunderid/quickstart/HomeScreen.kt @@ -0,0 +1,730 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +package dev.thunderid.quickstart + +import android.graphics.BitmapFactory +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import dev.thunderid.android.R +import dev.thunderid.compose.LocalThunderID +import dev.thunderid.compose.components.actions.SignOutButton +import dev.thunderid.compose.components.presentation.user.ChangeCredential +import dev.thunderid.compose.components.presentation.user.UserAvatar +import dev.thunderid.compose.components.presentation.user.UserProfile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +// ───────────────────────────────────────────────────────────────────────────── +// Design tokens +// ───────────────────────────────────────────────────────────────────────────── + +private val PrimaryBlue = Color(0xFF3688FF) +private val SuccessGreen = Color(0xFF2FBD6B) +private val DarkBg = Color(0xFF080F1C) +private val LightBg = Color(0xFFF7F9FC) +private val BorderLight = Color(0xFFDDE3EC) +private val TextPrimary = Color(0xFF05213F) +private val TextMuted = Color(0xFF5A7085) +private val TokenDarkBg = Color(0xFF0B1120) + +// JWT color coding +private val JwtHeaderColor = Color(0xFFFF7B72) +private val JwtPayloadColor = Color(0xFF79C0FF) +private val JwtSignatureColor = Color(0xFF3FB950) + +// ───────────────────────────────────────────────────────────────────────────── +// Root composable +// ───────────────────────────────────────────────────────────────────────────── + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeScreen() { + var screen by remember { mutableStateOf("home") } + val thunder = LocalThunderID.current + + when (screen) { + "home" -> HomeTab(onNavigate = { screen = it }) + "profile" -> ProfileScreen(onBack = { screen = "home" }) + "token" -> TokenDebugScreen(onBack = { screen = "home" }) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Home tab +// ───────────────────────────────────────────────────────────────────────────── + +@Composable +private fun HomeTab(onNavigate: (String) -> Unit) { + val thunder = LocalThunderID.current + + val dateLabel = remember { + SimpleDateFormat("EEEE, MMMM d", Locale.ENGLISH).format(Date()).uppercase() + } + val greetingName = remember(thunder.user) { + val given = thunder.user?.get("given_name") as? String + given?.takeIf { it.isNotBlank() } + ?: thunder.user?.email?.substringBefore("@")?.takeIf { it.isNotBlank() } + ?: "there" + } + val greeting = remember(greetingName) { + val hour = java.util.Calendar.getInstance().get(java.util.Calendar.HOUR_OF_DAY) + val timeOfDay = when { + hour < 12 -> "morning" + hour < 17 -> "afternoon" + else -> "evening" + } + "Good $timeOfDay, $greetingName." + } + + val claims = thunder.user?.claims + val authTime = remember(claims) { claimAsEpochSeconds(claims, "auth_time") } + val exp = remember(claims) { claimAsEpochSeconds(claims, "exp") } + val organisationName = remember(thunder.user) { + runCatching { thunder.client.getConfiguration().organizationHandle } + .getOrNull() + ?.takeIf { it.isNotBlank() } + ?: "Default" + } + + var nowSeconds by remember { mutableStateOf(System.currentTimeMillis() / 1000) } + LaunchedEffect(exp) { + while (exp != null) { + nowSeconds = System.currentTimeMillis() / 1000 + delay(1000) + } + } + + val signedInAtLabel = remember(authTime) { formatSignedInAt(authTime) } + val expiresInLabel = remember(exp, nowSeconds) { formatExpiresIn(exp, nowSeconds) } + + Column( + modifier = Modifier + .fillMaxSize() + .background(LightBg) + .verticalScroll(rememberScrollState()), + ) { + Spacer(Modifier.height(30.dp)) + + // Hero: avatar + (date/session row, greeting row) + Row( + modifier = Modifier.padding(horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + UserAvatar(size = 52.dp) + Spacer(Modifier.width(14.dp)) + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = dateLabel, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = TextMuted, + letterSpacing = 1.sp, + ) + Spacer(Modifier.width(6.dp)) + Box( + modifier = Modifier + .size(3.dp) + .background(BorderLight, CircleShape), + ) + Spacer(Modifier.width(6.dp)) + Box( + modifier = Modifier + .size(6.dp) + .background(SuccessGreen, CircleShape), + ) + Spacer(Modifier.width(5.dp)) + Text( + text = "Session active", + fontSize = 12.sp, + color = SuccessGreen, + fontWeight = FontWeight.Medium, + ) + } + Spacer(Modifier.height(4.dp)) + Text( + text = greeting, + fontSize = 22.sp, + fontWeight = FontWeight.Bold, + color = TextPrimary, + ) + } + } + + Spacer(Modifier.height(24.dp)) + + // Stats row + Row( + modifier = Modifier + .padding(horizontal = 24.dp) + .fillMaxWidth() + .border(1.dp, BorderLight, RoundedCornerShape(12.dp)) + .padding(vertical = 18.dp), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + ) { + StatColumn(value = signedInAtLabel, label = "SIGNED IN AT") + VerticalDivider(modifier = Modifier.height(36.dp), color = BorderLight) + StatColumn(value = expiresInLabel, label = "EXPIRES IN") + VerticalDivider(modifier = Modifier.height(36.dp), color = BorderLight) + StatColumn(value = organisationName, label = "ORGANISATION") + } + + Spacer(Modifier.height(28.dp)) + + // "What's next" section + Text( + text = "WHAT'S NEXT", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + color = TextMuted, + letterSpacing = 1.5.sp, + modifier = Modifier.padding(horizontal = 24.dp), + ) + + Spacer(Modifier.height(8.dp)) + + val steps = listOf( + NextStep( + number = "01", + title = "Explore use cases", + subtitle = "See what you can build β€” auth flows for web, mobile, APIs, and agents.", + href = "https://thunderid.dev/docs/next/use-cases/overview/", + ), + NextStep( + number = "02", + title = "Learn about flows", + subtitle = "Understand how authorization code, PKCE, client credentials, and device flows work.", + href = "https://thunderid.dev/docs/next/guides/flows/what-are-flows/", + ), + NextStep( + number = "03", + title = "Style your experience", + subtitle = "Customize the login UI, branding, and email templates to match your product.", + href = "https://thunderid.dev/docs/next/guides/design/overview/", + ), + NextStep( + number = "04", + title = "Explore SDK APIs", + subtitle = "Full Android SDK reference β€” composables, state objects, and configuration options.", + href = "https://thunderid.dev/docs/next/sdks/android/overview/", + ), + ) + + steps.forEach { step -> + StepRow(number = step.number, title = step.title, subtitle = step.subtitle, href = step.href) + } + + Spacer(Modifier.height(20.dp)) + + // Action rows + ActionRow(label = "Manage Account", onClick = { onNavigate("profile") }) + ActionRow(label = "Token Debug", onClick = { onNavigate("token") }) + + // Sign out + Box( + modifier = Modifier + .fillMaxWidth() + .border(width = 1.dp, color = BorderLight, shape = RoundedCornerShape(0.dp)) + .padding(horizontal = 24.dp, vertical = 4.dp), + ) { + SignOutButton(modifier = Modifier.fillMaxWidth()) + } + + Spacer(Modifier.height(40.dp)) + } +} + +@Composable +private fun StatColumn(value: String, label: String) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = value, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = TextPrimary, + ) + Spacer(Modifier.height(2.dp)) + Text( + text = label, + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + color = TextMuted, + letterSpacing = 0.8.sp, + ) + } +} + +private data class NextStep(val number: String, val title: String, val subtitle: String, val href: String) + +@Composable +private fun StepRow(number: String, title: String, subtitle: String, href: String) { + val uriHandler = LocalUriHandler.current + Row( + modifier = Modifier + .fillMaxWidth() + .border(width = 0.dp, color = Color.Transparent) + .clickable(onClick = { uriHandler.openUri(href) }) + .padding(horizontal = 24.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Row(verticalAlignment = Alignment.Top) { + Text( + text = number, + fontSize = 9.sp, + fontFamily = FontFamily.Monospace, + color = PrimaryBlue, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(top = 2.dp), + ) + Spacer(Modifier.width(14.dp)) + Column { + Text(text = title, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = TextPrimary) + Spacer(Modifier.height(2.dp)) + Text(text = subtitle, fontSize = 13.sp, color = TextMuted) + } + } + Text(text = "β€Ί", fontSize = 18.sp, color = TextMuted, modifier = Modifier.padding(start = 8.dp)) + } + Box( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .background(BorderLight), + ) +} + +@Composable +private fun ActionRow(label: String, onClick: () -> Unit) { + Box( + modifier = Modifier + .fillMaxWidth() + .border(width = 1.dp, color = BorderLight, shape = RoundedCornerShape(0.dp)) + .clickable(onClick = onClick) + .padding(horizontal = 24.dp, vertical = 16.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = label, + fontSize = 15.sp, + color = TextPrimary, + fontWeight = FontWeight.Medium, + ) + Text(text = "β€Ί", fontSize = 20.sp, color = TextMuted) + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Profile screen +// ───────────────────────────────────────────────────────────────────────────── + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ProfileScreen(onBack: () -> Unit) { + Column( + modifier = Modifier + .fillMaxSize() + .background(LightBg) + .verticalScroll(rememberScrollState()), + ) { + Spacer(Modifier.height(30.dp)) + + // Back button and page title on the same line, title aligned to the trailing edge. + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = Modifier.clickable(onClick = onBack), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = "β€Ή", fontSize = 20.sp, color = PrimaryBlue) + Spacer(Modifier.width(4.dp)) + Text(text = "Home", fontSize = 15.sp, color = PrimaryBlue, fontWeight = FontWeight.Medium) + } + + Text( + text = "Manage Account", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = TextPrimary, + ) + } + + Spacer(Modifier.height(12.dp)) + + // The styled UserProfile component owns its own data loading, editing and card + // design end to end β€” the sample just drops it in, no BaseUserProfile customization. + UserProfile(modifier = Modifier.fillMaxWidth()) + + Spacer(Modifier.height(24.dp)) + + // "Security" groups ChangeCredential the same way UserProfile's own "Profile info" header + // groups its card β€” ChangeCredential itself stays header-less so it composes into either + // a labelled group like this or on its own. + Text( + text = "SECURITY", + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = 0.4.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 20.dp), + ) + + ChangeCredential(modifier = Modifier.fillMaxWidth()) + + Spacer(Modifier.height(40.dp)) + } +} + +@Composable +private fun SectionHeader(title: String) { + Text( + text = title, + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + color = TextMuted, + letterSpacing = 1.5.sp, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), + ) +} + +@Composable +private fun DetailCard(content: @Composable () -> Unit) { + Column( + modifier = Modifier + .padding(horizontal = 24.dp) + .fillMaxWidth() + .border(1.dp, BorderLight, RoundedCornerShape(12.dp)) + .background(Color.White, RoundedCornerShape(12.dp)) + .clip(RoundedCornerShape(12.dp)), + ) { + content() + } +} + +@Composable +private fun DetailRow(label: String, content: @Composable () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = label, + fontSize = 13.sp, + color = TextMuted, + modifier = Modifier.width(100.dp), + ) + Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) { + content() + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Token Debug screen +// ───────────────────────────────────────────────────────────────────────────── + +@Composable +private fun TokenDebugScreen(onBack: () -> Unit) { + val thunder = LocalThunderID.current + var token by remember { mutableStateOf("") } + val clipboardManager = LocalClipboardManager.current + + LaunchedEffect(Unit) { + try { + token = thunder.client.getAccessToken() + } catch (_: Exception) {} + } + + val decoded = remember(token) { if (token.isNotEmpty()) decodeJwtPayload(token) else emptyMap() } + val rawJson = decoded["raw"] as? String ?: "" + val parts = (decoded["parts"] as? List<*>)?.map { it.toString() } ?: emptyList() + + val issuer = remember(rawJson) { + Regex("\"iss\"\\s*:\\s*\"([^\"]+)\"").find(rawJson)?.groupValues?.get(1) ?: "β€”" + } + val scopes = remember(rawJson) { + Regex("\"scope\"\\s*:\\s*\"([^\"]+)\"").find(rawJson)?.groupValues?.get(1) ?: "β€”" + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(LightBg) + .verticalScroll(rememberScrollState()), + ) { + Spacer(Modifier.height(56.dp)) + + // Back button + Row( + modifier = Modifier + .padding(horizontal = 24.dp) + .clickable(onClick = onBack), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = "β€Ή", fontSize = 20.sp, color = PrimaryBlue) + Spacer(Modifier.width(4.dp)) + Text(text = "Home", fontSize = 15.sp, color = PrimaryBlue, fontWeight = FontWeight.Medium) + } + + Spacer(Modifier.height(20.dp)) + + Column(modifier = Modifier.padding(horizontal = 24.dp)) { + Text(text = "Token debug", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = TextPrimary) + Spacer(Modifier.height(2.dp)) + Text(text = "Access token and claims", fontSize = 13.sp, color = TextMuted) + } + + Spacer(Modifier.height(16.dp)) + + // Access token display + Column(modifier = Modifier.padding(horizontal = 24.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = "ACCESS TOKEN", fontSize = 10.sp, fontWeight = FontWeight.Bold, color = TextMuted, letterSpacing = 1.sp) + OutlinedButton( + onClick = { if (token.isNotEmpty()) clipboardManager.setText(AnnotatedString(token)) }, + modifier = Modifier.height(28.dp), + shape = RoundedCornerShape(6.dp), + border = androidx.compose.foundation.BorderStroke(1.dp, BorderLight), + colors = ButtonDefaults.outlinedButtonColors(contentColor = TextMuted), + contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 10.dp, vertical = 0.dp), + ) { + Text(text = "Copy", fontSize = 11.sp) + } + } + Spacer(Modifier.height(8.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .background(TokenDarkBg, RoundedCornerShape(10.dp)) + .padding(14.dp), + ) { + if (token.isNotEmpty() && parts.size == 3) { + val annotated = buildAnnotatedString { + withStyle(SpanStyle(color = JwtHeaderColor)) { append(parts[0]) } + withStyle(SpanStyle(color = Color(0xFF8B8B8B))) { append(".") } + withStyle(SpanStyle(color = JwtPayloadColor)) { append(parts[1]) } + withStyle(SpanStyle(color = Color(0xFF8B8B8B))) { append(".") } + withStyle(SpanStyle(color = JwtSignatureColor)) { append(parts[2]) } + } + Text( + text = annotated, + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + lineHeight = 16.sp, + ) + } else { + Text( + text = if (token.isEmpty()) "Loading…" else token, + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + color = Color(0xFF79C0FF), + lineHeight = 16.sp, + ) + } + } + } + + Spacer(Modifier.height(20.dp)) + + // JWT Payload + Column(modifier = Modifier.padding(horizontal = 24.dp)) { + Text(text = "JWT PAYLOAD", fontSize = 10.sp, fontWeight = FontWeight.Bold, color = TextMuted, letterSpacing = 1.sp) + Spacer(Modifier.height(8.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .background(TokenDarkBg, RoundedCornerShape(10.dp)) + .padding(14.dp), + ) { + Text( + text = if (rawJson.isNotEmpty()) prettyPrintJson(rawJson) else "β€”", + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + color = JwtPayloadColor, + lineHeight = 18.sp, + ) + } + } + + Spacer(Modifier.height(20.dp)) + + // Issuer and Scopes + SectionHeader(title = "TOKEN DETAILS") + DetailCard { + DetailRow(label = "Issuer") { + Text( + text = issuer, + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + color = TextMuted, + textAlign = TextAlign.End, + ) + } + Box(modifier = Modifier.fillMaxWidth().height(1.dp).background(BorderLight)) + DetailRow(label = "Scopes") { + Text( + text = scopes, + fontSize = 12.sp, + color = TextMuted, + textAlign = TextAlign.End, + ) + } + } + + Spacer(Modifier.height(40.dp)) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Helpers +// ───────────────────────────────────────────────────────────────────────────── + +private fun decodeJwtPayload(token: String): Map { + return try { + val parts = token.split(".") + if (parts.size != 3) return emptyMap() + val padded = parts[1].let { it + "=".repeat((4 - it.length % 4) % 4) } + val json = String(android.util.Base64.decode(padded, android.util.Base64.URL_SAFE)) + mapOf("raw" to json, "parts" to parts) + } catch (e: Exception) { + emptyMap() + } +} + +private fun prettyPrintJson(json: String): String { + return try { + val sb = StringBuilder() + var indent = 0 + var inString = false + for (ch in json) { + when { + ch == '"' && !inString -> { inString = true; sb.append(ch) } + ch == '"' && inString -> { inString = false; sb.append(ch) } + inString -> sb.append(ch) + ch == '{' || ch == '[' -> { + sb.append(ch) + sb.append('\n') + indent++ + repeat(indent) { sb.append(" ") } + } + ch == '}' || ch == ']' -> { + sb.append('\n') + indent-- + repeat(indent) { sb.append(" ") } + sb.append(ch) + } + ch == ',' -> { + sb.append(ch) + sb.append('\n') + repeat(indent) { sb.append(" ") } + } + ch == ':' -> sb.append(": ") + ch != ' ' && ch != '\n' && ch != '\r' && ch != '\t' -> sb.append(ch) + } + } + sb.toString() + } catch (_: Exception) { + json + } +} + +private fun claimAsEpochSeconds(claims: Map?, key: String): Long? { + return when (val value = claims?.get(key)) { + is Number -> value.toLong() + is String -> value.toLongOrNull() + else -> null + } +} + +private fun formatSignedInAt(authTimeSeconds: Long?): String { + if (authTimeSeconds == null) return "β€”" + val formatter = SimpleDateFormat("h:mm a", Locale.getDefault()) + return formatter.format(Date(authTimeSeconds * 1000)) +} + +private fun formatExpiresIn(expSeconds: Long?, nowSeconds: Long): String { + if (expSeconds == null) return "β€”" + val remaining = expSeconds - nowSeconds + if (remaining <= 0) return "Expired" + val minutes = remaining / 60 + val seconds = remaining % 60 + return if (remaining < 3600) { + "${minutes}m ${seconds}s" + } else { + val hours = remaining / 3600 + val remMinutes = (remaining % 3600) / 60 + "${hours}h ${remMinutes}m" + } +} + diff --git a/quickstarts/android/src/main/kotlin/dev/thunderid/quickstart/MainActivity.kt b/quickstarts/android/src/main/kotlin/dev/thunderid/quickstart/MainActivity.kt new file mode 100644 index 0000000..a70378b --- /dev/null +++ b/quickstarts/android/src/main/kotlin/dev/thunderid/quickstart/MainActivity.kt @@ -0,0 +1,70 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +package dev.thunderid.quickstart + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color +import dev.thunderid.android.EncryptedStorageAdapter +import dev.thunderid.android.ThunderIDConfig +import dev.thunderid.android.auth.FederatedAuthSession +import dev.thunderid.compose.ThunderIDProvider + +// Matches the ThunderID blue used throughout AuthScreen/HomeScreen β€” the SDK's form +// fields and bottom sheet read `MaterialTheme.colorScheme.primary`, so leaving this at +// Material3's baseline (or an unrelated placeholder brand color) makes them look +// mismatched/pink against the rest of the sample. +private val ThunderIDPrimary = Color(0xFF3688FF) + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val attestationEnabled = BuildConfig.THUNDERID_ATTESTATION_ENABLED + val integrityTokenProvider = + PlayIntegrityTokenProvider(applicationContext, BuildConfig.THUNDERID_CLOUD_PROJECT_NUMBER) + + val config = ThunderIDConfig( + baseUrl = BuildConfig.THUNDERID_BASE_URL, + scopes = listOf("openid", "profile", "email"), + applicationId = BuildConfig.THUNDERID_APPLICATION_ID.takeIf { it.isNotBlank() }, + attestationEnabled = attestationEnabled, + attestationTokenProvider = if (attestationEnabled) integrityTokenProvider::requestToken else null, + storage = EncryptedStorageAdapter(this), + allowInsecureConnections = BuildConfig.DEBUG, + ) + + val colorScheme = lightColorScheme( + primary = ThunderIDPrimary, + secondary = Color(0xFF345F91), + surfaceVariant = Color(0xFFF2F2F2), + onSurfaceVariant = Color(0xFF49454F), + outlineVariant = Color(0x1F1D1B20), + ) + + setContent { + MaterialTheme(colorScheme = colorScheme) { + ThunderIDProvider(config = config) { + QuickstartApp(applicationId = BuildConfig.THUNDERID_APPLICATION_ID) + } + } + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + intent.data?.let(FederatedAuthSession::onRedirect) + } + + override fun onResume() { + super.onResume() + // A callback arrives in onNewIntent before this runs, so a sign-in still pending here means + // the user dismissed the browser. + FederatedAuthSession.cancelIfPending() + } +} diff --git a/quickstarts/android/src/main/kotlin/dev/thunderid/quickstart/PlayIntegrityTokenProvider.kt b/quickstarts/android/src/main/kotlin/dev/thunderid/quickstart/PlayIntegrityTokenProvider.kt new file mode 100644 index 0000000..c060f83 --- /dev/null +++ b/quickstarts/android/src/main/kotlin/dev/thunderid/quickstart/PlayIntegrityTokenProvider.kt @@ -0,0 +1,44 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +package dev.thunderid.quickstart + +import android.content.Context +import com.google.android.gms.tasks.Task +import com.google.android.play.core.integrity.IntegrityManagerFactory +import com.google.android.play.core.integrity.StandardIntegrityManager.PrepareIntegrityTokenRequest +import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenProvider +import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenRequest +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +/** + * Mints Google Play Integrity Standard API tokens for [dev.thunderid.android.ThunderIDConfig.attestationTokenProvider]. + * The warm-up ([StandardIntegrityTokenProvider]) is cached and reused across sign-in attempts. + */ +class PlayIntegrityTokenProvider( + private val context: Context, + private val cloudProjectNumber: Long, +) { + private var tokenProvider: StandardIntegrityTokenProvider? = null + + suspend fun requestToken(): String { + val provider = tokenProvider ?: prepareTokenProvider().also { tokenProvider = it } + return provider.request(StandardIntegrityTokenRequest.builder().build()).await().token() + } + + private suspend fun prepareTokenProvider(): StandardIntegrityTokenProvider = + IntegrityManagerFactory.createStandard(context) + .prepareIntegrityToken( + PrepareIntegrityTokenRequest.builder() + .setCloudProjectNumber(cloudProjectNumber) + .build(), + ).await() +} + +private suspend fun Task.await(): T = + suspendCancellableCoroutine { cont -> + addOnSuccessListener { cont.resume(it) } + addOnFailureListener { cont.resumeWithException(it) } + } diff --git a/quickstarts/android/src/main/kotlin/dev/thunderid/quickstart/RootView.kt b/quickstarts/android/src/main/kotlin/dev/thunderid/quickstart/RootView.kt new file mode 100644 index 0000000..2bd9683 --- /dev/null +++ b/quickstarts/android/src/main/kotlin/dev/thunderid/quickstart/RootView.kt @@ -0,0 +1,58 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +package dev.thunderid.quickstart + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import dev.thunderid.compose.LocalThunderID + +@Composable +fun QuickstartApp(applicationId: String) { + val thunder = LocalThunderID.current + when { + thunder.error != null && !thunder.isInitialized -> ErrorScreen(thunder.error!!) + !thunder.isInitialized || thunder.isLoading -> LoadingScreen() + thunder.error != null -> ErrorScreen(thunder.error!!) + thunder.isSignedIn -> HomeScreen() + else -> AuthScreen(applicationId = applicationId) + } +} + +@Composable +private fun LoadingScreen() { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + CircularProgressIndicator() + Spacer(Modifier.height(12.dp)) + Text("Starting ThunderID…", color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } +} + +@Composable +private fun ErrorScreen(message: String) { + Box( + Modifier + .fillMaxSize() + .padding(24.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = "Configuration error: $message\n\nCheck your .env values.", + textAlign = TextAlign.Center, + ) + } +} diff --git a/quickstarts/android/src/main/res/drawable-night/ic_launcher_foreground.png b/quickstarts/android/src/main/res/drawable-night/ic_launcher_foreground.png new file mode 100644 index 0000000..7aa6192 Binary files /dev/null and b/quickstarts/android/src/main/res/drawable-night/ic_launcher_foreground.png differ diff --git a/quickstarts/android/src/main/res/drawable/ic_launcher_foreground.png b/quickstarts/android/src/main/res/drawable/ic_launcher_foreground.png new file mode 100644 index 0000000..ddd01af Binary files /dev/null and b/quickstarts/android/src/main/res/drawable/ic_launcher_foreground.png differ diff --git a/quickstarts/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/quickstarts/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..5ed0a2d --- /dev/null +++ b/quickstarts/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/quickstarts/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/quickstarts/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..5ed0a2d --- /dev/null +++ b/quickstarts/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/quickstarts/android/src/main/res/mipmap-hdpi/ic_launcher.png b/quickstarts/android/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..b8494ca Binary files /dev/null and b/quickstarts/android/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/quickstarts/android/src/main/res/mipmap-hdpi/ic_launcher_round.png b/quickstarts/android/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..b8494ca Binary files /dev/null and b/quickstarts/android/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/quickstarts/android/src/main/res/mipmap-mdpi/ic_launcher.png b/quickstarts/android/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..ff4a761 Binary files /dev/null and b/quickstarts/android/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/quickstarts/android/src/main/res/mipmap-mdpi/ic_launcher_round.png b/quickstarts/android/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..ff4a761 Binary files /dev/null and b/quickstarts/android/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/quickstarts/android/src/main/res/mipmap-xhdpi/ic_launcher.png b/quickstarts/android/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..3604b63 Binary files /dev/null and b/quickstarts/android/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/quickstarts/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/quickstarts/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..3604b63 Binary files /dev/null and b/quickstarts/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/quickstarts/android/src/main/res/mipmap-xxhdpi/ic_launcher.png b/quickstarts/android/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..7943316 Binary files /dev/null and b/quickstarts/android/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/quickstarts/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/quickstarts/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..7943316 Binary files /dev/null and b/quickstarts/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/quickstarts/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/quickstarts/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..b8da097 Binary files /dev/null and b/quickstarts/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/quickstarts/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/quickstarts/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b8da097 Binary files /dev/null and b/quickstarts/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/quickstarts/android/src/main/res/values-night/ic_launcher_colors.xml b/quickstarts/android/src/main/res/values-night/ic_launcher_colors.xml new file mode 100644 index 0000000..0cfdac6 --- /dev/null +++ b/quickstarts/android/src/main/res/values-night/ic_launcher_colors.xml @@ -0,0 +1,4 @@ + + + #0F172A + diff --git a/quickstarts/android/src/main/res/values/ic_launcher_colors.xml b/quickstarts/android/src/main/res/values/ic_launcher_colors.xml new file mode 100644 index 0000000..f42ada6 --- /dev/null +++ b/quickstarts/android/src/main/res/values/ic_launcher_colors.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + diff --git a/quickstarts/android/src/main/res/xml/network_security_config.xml b/quickstarts/android/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..931ccdc --- /dev/null +++ b/quickstarts/android/src/main/res/xml/network_security_config.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/quickstarts/android/thunderid-config/thunderid-config.yaml b/quickstarts/android/thunderid-config/thunderid-config.yaml new file mode 100644 index 0000000..f167a70 --- /dev/null +++ b/quickstarts/android/thunderid-config/thunderid-config.yaml @@ -0,0 +1,34 @@ +--- +resource_type: application +# Application used exclusively by the Android Quickstart E2E suite. It has no OAuth profile: the +# mobile SDKs authenticate app-natively via the Flow Execution API, so the sample app needs only +# the base URL and this application ID. +# +# The ID is fixed so the sample's Config.plist and the Maestro flows can be committed against a +# known value instead of discovering it at run time. +id: 019e5b10-1001-7a2b-9c3d-4e5f60718293 +ouHandle: default +name: Mobile Quickstart E2E App +description: Application for the ThunderID mobile SDK quickstart E2E suites +type: mobile +# A mobile application must normally prove its binary identity via platform attestation before it +# may initiate a flow directly, otherwise /flow/execute fails with FES-1016. Apple App Attest is +# unavailable in the iOS Simulator and Play Integrity requires real Play Services, so neither can +# be satisfied on the emulated devices this suite drives. devMode skips that check. +# +# Test-only. Never enable this on a real tenant: it removes the guarantee that the client +# initiating a flow is the genuine, unmodified app. +# +# NOTE: POST /import currently drops this block silently (it reports success, then stores +# attestation: null), so run-e2e.sh re-applies it over PUT /applications/{id}. Once the +# import path preserves it, that extra step can go. +attestation: + devMode: true +authFlowHandle: default-flow +# The suite registers a fresh user through the UI rather than relying on a seeded account, so +# self-service registration has to be enabled for this application. +registrationFlowHandle: default-flow +isRegistrationFlowEnabled: true +signOutFlowHandle: default-flow +allowedUserTypes: + - Person diff --git a/quickstarts/browser/.env.example b/quickstarts/browser/.env.example new file mode 100644 index 0000000..e38f402 --- /dev/null +++ b/quickstarts/browser/.env.example @@ -0,0 +1,5 @@ +# Base URL of your ThunderID deployment. ex: https://localhost:8090 +VITE_THUNDERID_BASE_URL=https://localhost:8090 + +# OAuth2 Client ID for this app. ThunderID Console -> your application -> Overview. +VITE_THUNDERID_CLIENT_ID=your-client-id-here diff --git a/quickstarts/browser/.gitignore b/quickstarts/browser/.gitignore new file mode 100644 index 0000000..9c97bbd --- /dev/null +++ b/quickstarts/browser/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +.env diff --git a/quickstarts/browser/.stackblitzrc b/quickstarts/browser/.stackblitzrc new file mode 100644 index 0000000..5f0a200 --- /dev/null +++ b/quickstarts/browser/.stackblitzrc @@ -0,0 +1,4 @@ +{ + "installDependencies": false, + "startCommand": "npm run prepare-dev && npm install && npm run dev" +} diff --git a/quickstarts/browser/README.md b/quickstarts/browser/README.md new file mode 100644 index 0000000..6738225 --- /dev/null +++ b/quickstarts/browser/README.md @@ -0,0 +1,37 @@ +# ThunderID Browser Quickstart + +Open in StackBlitz + +A minimal Vite + vanilla JS app demonstrating sign-in and sign-out with the ThunderID JavaScript SDK (`@thunderid/browser`). + +## Prerequisites + +- Node.js 18+ +- A running ThunderID instance (default: `https://localhost:8090`) +- A configured application with an authorized redirect URI set to your app's origin + +## Getting started + +1. Copy the environment file and fill in your values: + ```sh + cp .env.example .env + ``` + +2. Edit `.env` with your application's credentials: + ``` + VITE_THUNDERID_CLIENT_ID= + VITE_THUNDERID_BASE_URL=https://localhost:8090 + ``` + +3. Install dependencies and start the dev server: + ```sh + pnpm install + pnpm dev + ``` + +Open [http://localhost:5173](http://localhost:5173) in your browser. + +## Learn more + +- [ThunderID Docs](https://thunderid.dev/docs) +- [`@thunderid/browser` SDK reference](https://thunderid.dev/docs/sdks/javascript/browser) diff --git a/quickstarts/browser/index.html b/quickstarts/browser/index.html new file mode 100644 index 0000000..8e93376 --- /dev/null +++ b/quickstarts/browser/index.html @@ -0,0 +1,12 @@ + + + + + + ThunderID JavaScript Quickstart + + +
+ + + diff --git a/quickstarts/browser/package.json b/quickstarts/browser/package.json new file mode 100644 index 0000000..a2b4c2e --- /dev/null +++ b/quickstarts/browser/package.json @@ -0,0 +1,18 @@ +{ + "name": "@thunderid/browser-quickstart", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "prepare-dev": "node scripts/prepare-dev.cjs", + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@thunderid/browser": "1.3.0" + }, + "devDependencies": { + "vite": "^6.3.5" + } +} diff --git a/quickstarts/browser/pnpm-lock.yaml b/quickstarts/browser/pnpm-lock.yaml new file mode 100644 index 0000000..9ac98d2 --- /dev/null +++ b/quickstarts/browser/pnpm-lock.yaml @@ -0,0 +1,790 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@thunderid/browser': + specifier: 1.3.0 + version: 1.3.0 + devDependencies: + vite: + specifier: ^6.3.5 + version: 6.4.4 + +packages: + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-android-arm-eabi@4.64.9': + resolution: {integrity: sha512-qJmhckvOleXp8R+BaXiVW/S34/3M7pvN17/QcG90A74BDaSvKv8ffY+GYG+a/xU0vEoITzdMSiM7o9HZSQWbfA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.64.9': + resolution: {integrity: sha512-dqQgds39BNWGB2ughJ6n2LTQFXk8RAOwnXj6m387OHpygfD93QRS1ODIfpiWyAa9gsFB9l34pSqeLmA0Xk/VBw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.64.9': + resolution: {integrity: sha512-scrMx4SGikOSbWknLmPD0v6OeJ1Wih3kyRLdNZnlVbBeKVWp1nRTVcpM0IxDJdc3u8UjesPHTmnX649u8cQpBw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.64.9': + resolution: {integrity: sha512-fkwi2OQR+QWoViRFt08Bsca6G0e9pk/4z+JgmqHq6jSRTT0LtI27KRVK8bu++6L/CBtQ3OZePbqC3bcrwGzTqQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.64.9': + resolution: {integrity: sha512-8Apr0Khn0r2MUsDtcOMbp85fDdXsX/E+iLhO17WUjG24m3O8ChArqI0isI7CAdj9Ouq177PHtVtEUpMNE1C4Gw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.64.9': + resolution: {integrity: sha512-H+upy4DhBcBZlUsCAMyr8wR0Dg9kj8sGloVZdZ7va2heJM/5jnROxI47dtXseJjlD/VoQe6MrXhY3b5ikOq/Vw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.64.9': + resolution: {integrity: sha512-b50NS9TbAEK/b16vBoKrkeNwy+HvGBXmtBbzX4wcodWOh9dQZvRTN9rUyElIXa36QnwXhU9PAVVkZpqRzSi2Tg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.64.9': + resolution: {integrity: sha512-SdqcudB/FzjLESsRMgXhV+FXGtf6usxXwlMoRW7D/XS3f9CZ4T4wycRBu2h9bVl2AOaPpunoA8ftAM/MF4altQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.64.9': + resolution: {integrity: sha512-dg19FFRoovN+KZ+gMHBAq4Do8PgQmBcxpuhux57OuiR0QLavfn8uPBY8CP+UO50bO+xf0tK0GlqI/jld++5E6g==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.64.9': + resolution: {integrity: sha512-+jyv5ofGmpAvtrzLjSr/e4syskeikEp5L82bUzB76auwOgK1VNVwZV/F2g1Pes9bhTznbejOefAqPglHSKYmTA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.64.9': + resolution: {integrity: sha512-MKFXxqmkzCbtNvx/vjiJdss44eRtVabhCtDWcCFD9NHGwP+YkqnM/slnEsv/3fsoq9ukTtudraAjGhptrUPCVg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.64.9': + resolution: {integrity: sha512-lhARcOT6n7Ni9hpQRq8LZjbuoW+G/GXaW4Jfhx2MbmjnCj9XxLMRTceJIMpiMLQVWm+qxe7oq1sYvlIGOAW34Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.64.9': + resolution: {integrity: sha512-oORQzGxZ1oHghJwLdwc8Ni0SY9otILAeXsWdiImTF4F5HkWJUwh5s/269PF5tYILNC1z/3RwFsrgcHPguBcb0g==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.64.9': + resolution: {integrity: sha512-J2JQHQt7oR1uutd2OSwbV2Qpb2LI5gggPolbkUwDe7vn/0dnduZ0w6EIbGnZUAw7uIKaMgfVylcH6V+Aov8kRA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.64.9': + resolution: {integrity: sha512-bWaKLrfzeQ9mOzJJxXbPgw2RH6VAaTJrb/60Tqf7YSMxubG8780Hzm7Us+D6qVW8QRGJYl4VUvVXxnRB0o9zvQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.64.9': + resolution: {integrity: sha512-TFJyaCB9NsvSyvVGQgTSRD9XgxsAq99ivDDkt9KAySlrNW5Obsd1kQpvoCvBCOLq+JlJTjmtroLH0wp8M8mEsA==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.64.9': + resolution: {integrity: sha512-DvmOM5xz/wDnUJFVjqP32ahalxn7BDuvO25bpuFOrb50AIFCCcFPN4lWWUDD1c8Ddv9sMO9SXyqtOet8G8u9cQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.64.9': + resolution: {integrity: sha512-aFzokHK3yE00mzAvKm/PY76OsOOTjvoTGE82Vp89iBDyD2wUBlV4ZsR2tWkRECTEFk6TC4Yc3zaEWjPqq1vstQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.64.9': + resolution: {integrity: sha512-t/yMdeaaQUuNNpmT7taBmH2VvHzgT4YA/IaKVE3qVr4PYQemn/kmJkL4uG4UvTLcWXQd0MSoOa/3URYSWGmUEg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.64.9': + resolution: {integrity: sha512-4dnxKQ8GHARyjqAU87q5UWhywZTpIbR2VWhtACdC37GD1SGszmvb3TaBFVA4n+fnDHSKMut4ZJ63hVFwgnxVtg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.64.9': + resolution: {integrity: sha512-1qdQj6IrNoR0xFvrCQHmhBXe9HMbBGNTFi+3n4jKMt2rTx4k0aeAdraZU4WDOMfL9tAwS8/TRSuquvNuPQ+Gpw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.64.9': + resolution: {integrity: sha512-yc1GSEHIYhKGN5WSaJc+eMy8bZvzcvAmflEgFh8L3xcy8dYxGmfrudyBnBFde9f6JzOa1kFqocF8BHaWybjQoA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.64.9': + resolution: {integrity: sha512-VzHGvosAkLIzrwqvSgJBPDmGFTF/yE8OtMT4H5z8pM1NA0NiPpvTYnUh+9xPd+d5oy/wqF317FmJ1I8Kk8pp0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.64.9': + resolution: {integrity: sha512-FB77txy1PiAcJgG72nOLA6R0S3PRZahs8Spx0z2yseDUkoMmJcrQghiebJ2JHZ74cCOVlX/MYd4dUbJtJJ8fow==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.64.9': + resolution: {integrity: sha512-IoIgQLpK1DbOcQf1vX+i5oe5LXypsCVb1j/mU6BLAbOGmXH4E+j4kMfgZRwPYUohVBurNyVMgIKVM5dR6xA0kA==} + cpu: [x64] + os: [win32] + + '@thunderid/browser@1.3.0': + resolution: {integrity: sha512-O6DHlUyVFfBaNcJDEXACLU4NJMrNjDlqAuzesvJ+8a0Gl5g8WFK53rpx027e/rWICG0vcGe2UVZuhvZq5Z511A==} + + '@thunderid/javascript@1.4.0': + resolution: {integrity: sha512-qHfIFzXOQTN1nhuBT91Tr8bLPvfyEDa7gUQSL7GNcJkcyyKfJ77meIwr91lCme0ySFUnTeKl0Bk+R8iIJiKXBw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + core-js@3.42.0: + resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.30: + resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==} + engines: {node: ^10 || ^12 || >=14} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + rollup@4.64.9: + resolution: {integrity: sha512-mnPSQINChN7PpT+a5CI0/+87hm/+6zxSKBK1XonrzQ0zFnglaTjhkTP2Br4yNCTcLHUEvrAfZffH57jGf+8xBQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + stream-browserify@3.0.0: + resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@6.4.4: + resolution: {integrity: sha512-xGjU/SyjeNyb0E4KxXBvYz/1C+3JXuujvf2hXNOtnlcE+58o5ajd9N2/n2VhkiJvpCP2qQasnUpx5PXNeI+p1Q==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + +snapshots: + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@rollup/rollup-android-arm-eabi@4.64.9': + optional: true + + '@rollup/rollup-android-arm64@4.64.9': + optional: true + + '@rollup/rollup-darwin-arm64@4.64.9': + optional: true + + '@rollup/rollup-darwin-x64@4.64.9': + optional: true + + '@rollup/rollup-freebsd-arm64@4.64.9': + optional: true + + '@rollup/rollup-freebsd-x64@4.64.9': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.64.9': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.64.9': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.64.9': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.64.9': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.64.9': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.64.9': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-x64-musl@4.64.9': + optional: true + + '@rollup/rollup-openbsd-x64@4.64.9': + optional: true + + '@rollup/rollup-openharmony-arm64@4.64.9': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.64.9': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.64.9': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.64.9': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.64.9': + optional: true + + '@thunderid/browser@1.3.0': + dependencies: + '@thunderid/javascript': 1.4.0 + base64url: 3.0.1 + buffer: 6.0.3 + core-js: 3.42.0 + fast-sha256: 1.3.0 + jose: 6.2.3 + process: 0.11.10 + randombytes: 2.1.0 + stream-browserify: 3.0.0 + tslib: 2.8.1 + + '@thunderid/javascript@1.4.0': + dependencies: + jose: 6.2.3 + tslib: 2.8.1 + + '@types/estree@1.0.9': {} + + base64-js@1.5.1: {} + + base64url@3.0.1: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + core-js@3.42.0: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + fast-sha256@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + ieee754@1.2.1: {} + + inherits@2.0.4: {} + + jose@6.2.3: {} + + nanoid@3.3.20: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.30: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + process@0.11.10: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + rollup@4.64.9: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.64.9 + '@rollup/rollup-android-arm64': 4.64.9 + '@rollup/rollup-darwin-arm64': 4.64.9 + '@rollup/rollup-darwin-x64': 4.64.9 + '@rollup/rollup-freebsd-arm64': 4.64.9 + '@rollup/rollup-freebsd-x64': 4.64.9 + '@rollup/rollup-linux-arm-gnueabihf': 4.64.9 + '@rollup/rollup-linux-arm-musleabihf': 4.64.9 + '@rollup/rollup-linux-arm64-gnu': 4.64.9 + '@rollup/rollup-linux-arm64-musl': 4.64.9 + '@rollup/rollup-linux-loong64-gnu': 4.64.9 + '@rollup/rollup-linux-loong64-musl': 4.64.9 + '@rollup/rollup-linux-ppc64-gnu': 4.64.9 + '@rollup/rollup-linux-ppc64-musl': 4.64.9 + '@rollup/rollup-linux-riscv64-gnu': 4.64.9 + '@rollup/rollup-linux-riscv64-musl': 4.64.9 + '@rollup/rollup-linux-s390x-gnu': 4.64.9 + '@rollup/rollup-linux-x64-gnu': 4.64.9 + '@rollup/rollup-linux-x64-musl': 4.64.9 + '@rollup/rollup-openbsd-x64': 4.64.9 + '@rollup/rollup-openharmony-arm64': 4.64.9 + '@rollup/rollup-win32-arm64-msvc': 4.64.9 + '@rollup/rollup-win32-ia32-msvc': 4.64.9 + '@rollup/rollup-win32-x64-gnu': 4.64.9 + '@rollup/rollup-win32-x64-msvc': 4.64.9 + fsevents: 2.3.3 + + safe-buffer@5.2.1: {} + + source-map-js@1.2.2: {} + + stream-browserify@3.0.0: + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tslib@2.8.1: {} + + util-deprecate@1.0.2: {} + + vite@6.4.4: + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.30 + rollup: 4.64.9 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 diff --git a/quickstarts/browser/pnpm-workspace.yaml b/quickstarts/browser/pnpm-workspace.yaml new file mode 100644 index 0000000..151e558 --- /dev/null +++ b/quickstarts/browser/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + core-js: false + esbuild: true diff --git a/quickstarts/browser/scripts/prepare-dev.cjs b/quickstarts/browser/scripts/prepare-dev.cjs new file mode 100644 index 0000000..734e095 --- /dev/null +++ b/quickstarts/browser/scripts/prepare-dev.cjs @@ -0,0 +1,19 @@ +// 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'); +if (fs.existsSync(envExample) && !fs.existsSync(envTarget)) { + // 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. + const envContent = fs + .readFileSync(envExample, 'utf8') + .replace(/^([A-Z0-9_]+)=(your-\S*|generate-with-\S*|https:\/\/localhost:8090)$/gm, '$1='); + fs.writeFileSync(envTarget, envContent); +} diff --git a/quickstarts/browser/src/auth.js b/quickstarts/browser/src/auth.js new file mode 100644 index 0000000..bad532b --- /dev/null +++ b/quickstarts/browser/src/auth.js @@ -0,0 +1,18 @@ +import { ThunderIDBrowserClient } from '@thunderid/browser' + +const REQUIRED_ENV_VARS = ['VITE_THUNDERID_CLIENT_ID', 'VITE_THUNDERID_BASE_URL'] + +export const missingEnvVars = REQUIRED_ENV_VARS.filter((key) => !import.meta.env[key]) + +const auth = new ThunderIDBrowserClient() + +if (missingEnvVars.length === 0) { + await auth.initialize({ + clientId: import.meta.env.VITE_THUNDERID_CLIENT_ID, + baseUrl: import.meta.env.VITE_THUNDERID_BASE_URL, + afterSignInUrl: window.location.origin, + afterSignOutUrl: window.location.origin, + }) +} + +export default auth diff --git a/quickstarts/browser/src/components/nav.js b/quickstarts/browser/src/components/nav.js new file mode 100644 index 0000000..25b1ca7 --- /dev/null +++ b/quickstarts/browser/src/components/nav.js @@ -0,0 +1,171 @@ +const ICON_MOON = `` +const ICON_SUN = `` +const ICON_KEY = `` +const ICON_USER = `` +const ICON_CHEVRON_LEFT = `` +const ICON_CHEVRON_DOWN = `` +const ICON_TECH = `JS` + +function escapeHtml(str) { + if (str == null) return '' + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +function getInitials(user) { + const given = user?.given_name || '' + const family = user?.family_name || '' + if (given && family) return (given[0] + family[0]).toUpperCase() + const name = user?.name || user?.username || user?.email || '?' + const parts = name.trim().split(/\s+/) + if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase() + return name.slice(0, 2).toUpperCase() +} + +function getAvatarUrl(user) { + return user?.profile || user?.profileUrl || user?.picture || user?.URL || null +} + +function renderAvatar(user, className) { + const avatarUrl = getAvatarUrl(user) + if (avatarUrl) { + return `` + } + return `${escapeHtml(getInitials(user))}` +} + +function getFullName(user) { + const given = user?.given_name || '' + const family = user?.family_name || '' + if (given && family) return `${given} ${family}` + return user?.name || user?.username || 'User' +} + +export function renderSignedOutNav({ isDark, hideSignIn = false }) { + return ` + ` +} + +export function renderSignedInNav({ user, isDark, currentPage }) { + const name = escapeHtml(getFullName(user)) + const email = escapeHtml(user?.email || '') + + return ` + ` +} + +export function attachNavHandlers({ isDark, setIsDark, navigateTo, auth }) { + document.getElementById('dark-toggle')?.addEventListener('click', () => { + const next = !isDark() + setIsDark(next) + document.documentElement.classList.toggle('dark', next) + const dt = document.getElementById('dark-toggle') + if (dt) dt.innerHTML = next ? ICON_SUN : ICON_MOON + document.querySelectorAll('#hero-mark-top').forEach(el => + el.setAttribute('fill', next ? '#E8F4FF' : '#05213F') + ) + }) + + document.getElementById('nav-logo-btn')?.addEventListener('click', (e) => { + e.preventDefault() + navigateTo('home') + }) + + document.getElementById('nav-back-btn')?.addEventListener('click', () => { + navigateTo('home') + }) + + const trigger = document.getElementById('ud-trigger') + const menu = document.getElementById('ud-menu') + const wrapper = document.getElementById('ud-wrapper') + if (trigger && menu) { + trigger.addEventListener('click', (e) => { + e.stopPropagation() + menu.classList.toggle('ud-menu--open') + }) + document.addEventListener('click', (e) => { + if (!wrapper?.contains(e.target)) menu.classList.remove('ud-menu--open') + }, { once: true }) + } + + document.getElementById('ud-token-debug')?.addEventListener('click', () => { + menu?.classList.remove('ud-menu--open') + navigateTo('token') + }) + + document.getElementById('ud-manage-profile')?.addEventListener('click', () => { + menu?.classList.remove('ud-menu--open') + navigateTo('account') + }) + + document.getElementById('ud-sign-out')?.addEventListener('click', () => { + auth.signOut() + }) +} + +export function attachSignedOutNavHandlers({ auth }) { + document.getElementById('sign-in-btn')?.addEventListener('click', () => auth.signIn()) + document.getElementById('dark-toggle')?.addEventListener('click', () => { + const next = !document.documentElement.classList.contains('dark') + document.documentElement.classList.toggle('dark', next) + const dt = document.getElementById('dark-toggle') + if (dt) dt.innerHTML = next ? ICON_SUN : ICON_MOON + document.querySelectorAll('#hero-mark-top').forEach(el => + el.setAttribute('fill', next ? '#E8F4FF' : '#05213F') + ) + }) +} diff --git a/quickstarts/browser/src/components/profileFields.js b/quickstarts/browser/src/components/profileFields.js new file mode 100644 index 0000000..f0fd045 --- /dev/null +++ b/quickstarts/browser/src/components/profileFields.js @@ -0,0 +1,305 @@ +import { deepMerge, getUsersMe, getUsersMeMeta, updateMeProfile } from '@thunderid/browser' + +const ICON_PENCIL = `` + +// Attributes that are always read-only regardless of schema mutability +const ALWAYS_READONLY_KEYS = [ + 'attributes', + 'id', + 'isReadOnly', + 'isReadonly', + 'ouId', + 'sub', + 'username', + 'userName', + 'user_name', +] + +export function escapeHtml(str) { + if (str == null) return '' + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +function formatLabel(key) { + return key + .split(/(?=[A-Z])|[_.]/) + .filter(Boolean) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' ') +} + +function fieldLabel(key, schemaEntry) { + return schemaEntry?.displayName || formatLabel(key) +} + +function isFieldEditable(key, schemaEntry) { + if (ALWAYS_READONLY_KEYS.includes(key)) return false + if (!schemaEntry) return false + if (schemaEntry.credential) return false + if (schemaEntry.readOnly || schemaEntry.mutability === 'READ_ONLY') return false + return true +} + +function getAvatarUrl(user) { + return user?.profile || user?.profileUrl || user?.picture || user?.URL || null +} + +// Deterministic gradient background derived from a name, matching `@thunderid/react`'s +// `Avatar` `background="random"` (default) behavior β€” same hash and HSL formula, so a +// given user gets the same avatar color here as they would in the React/Vue quickstarts. +function generateAvatarBackground(name) { + const hash = name.split('').reduce((acc, char) => ((acc << 5) - acc + char.charCodeAt(0)) & 0xffffffff, 0) + const seed = Math.abs(hash) + const hue1 = seed % 360 + const hue2 = (hue1 + 60 + (seed % 120)) % 360 + const saturation = 70 + (seed % 20) + const lightness1 = 55 + (seed % 15) + const lightness2 = 60 + (seed % 15) + const angle = 45 + (seed % 91) + return `linear-gradient(${angle}deg, hsl(${hue1}, ${saturation}%, ${lightness1}%), hsl(${hue2}, ${saturation}%, ${lightness2}%))` +} + +function getInitials(user) { + const given = user?.given_name || '' + const family = user?.family_name || '' + if (given && family) return (given[0] + family[0]).toUpperCase() + const name = user?.name || user?.username || user?.email || '?' + const parts = name.trim().split(/\s+/) + if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase() + return name.slice(0, 2).toUpperCase() +} + +// `getDisplayName`: first + last name, falling back to +// username, email, then the `name` attribute. +function getDisplayName(user, attributes) { + const given = attributes?.given_name || user?.given_name + const family = attributes?.family_name || user?.family_name + if (given && family) return `${given} ${family}` + return user?.username || user?.email || attributes?.name || 'User' +} + +function renderAvatarInner(user, displayName) { + const avatarUrl = getAvatarUrl(user) + if (avatarUrl) { + return { className: '', html: `` } + } + return { + className: 'has-gradient', + html: escapeHtml(getInitials(user)), + style: `background:${generateAvatarBackground(displayName || 'User')}`, + } +} + +export function createFetcher(auth) { + return async (url, config) => { + const token = await auth.getAccessToken() + return fetch(url, { + ...config, + headers: { ...config.headers, Authorization: `Bearer ${token}` }, + }) + } +} + +// Fetches the schema and the current attribute values needed to render and validate the profile view. +export async function fetchProfileFormContext({ baseUrl, auth }) { + const fetcher = createFetcher(auth) + + // A failed fetch stays `null`, not `{}` β€” `supportsCredential` treats those differently: an + // unresolved schema is "not known yet" (every credential stays available), while `{}` is a + // known schema declaring nothing (every credential becomes unavailable). Collapsing a fetch + // failure into `{}` would make a transient network error look identical to "no credentials + // configured". + const [metaRes, profile] = await Promise.all([ + getUsersMeMeta({ baseUrl, fetcher }).catch(() => null), + getUsersMe({ baseUrl, fetcher }).catch(() => null), + ]) + + return { schema: metaRes?.schema ?? null, profile } +} + +function renderFieldRow(key, schemaEntry, value) { + if (schemaEntry?.credential) return '' + + const label = fieldLabel(key, schemaEntry) + const editable = isFieldEditable(key, schemaEntry) + const hasValue = value !== undefined && value !== null && value !== '' + + // BaseUserProfile `shouldShow`: an empty read-only field is hidden entirely rather than rendered as a dash. + if (!hasValue && !editable) return '' + + const displayValue = hasValue ? escapeHtml(String(value)) : `Enter your ${label.toLowerCase()}` + + return ` +
+
${escapeHtml(label)}
+
+ ${displayValue} + ${editable ? `` : ''} +
+
` +} + +// Renders the "Personal info" section: avatar/name summary plus the schema-driven, +// per-field editable rows. Used by the account page's Personal info tab. +export function renderProfileFields(user, { schema = {}, profile } = {}) { + const attributes = profile?.attributes || {} + const displayName = getDisplayName(user, attributes) + const avatar = renderAvatarInner(user, displayName) + const email = escapeHtml(user?.email || user?.username || '') + + // `schema` may be `null` (the meta fetch failed) β€” a default parameter only covers + // `undefined`, not an explicit `null`, so this still needs its own fallback. + const rows = Object.entries(schema || {}) + .map(([key, schemaEntry]) => renderFieldRow(key, schemaEntry, attributes[key])) + .join('') + + return ` +
+
${avatar.html}
+
+
${escapeHtml(displayName)}
+ ${email ? `
${email}
` : ''} +
+
+ +
${rows}
` +} + +// Validates a field value against its schema entry (required + regex), matching +// BaseUserProfile `handleFieldSave`. Returns an error message, or +// `null` when valid. +function validateField(schemaEntry, label, value) { + if (!schemaEntry) return null + + if (schemaEntry.required && !value) { + return `${label} is required.` + } + + if (schemaEntry.regex && value) { + try { + if (!new RegExp(schemaEntry.regex).test(value)) { + return `${label} is not in a valid format.` + } + } catch { + // Invalid regex on the schema itself β€” nothing to enforce client-side. + } + } + + return null +} + +// Wires up inline per-field editing for the rows rendered by `renderProfileFields`. Safe to +// call once after that markup is in the DOM. +export function attachProfileFieldHandlers({ user, auth, schema, profile, onSaved }) { + // `schema` may be `null` (the meta fetch failed) β€” a default parameter only covers + // `undefined`, not an explicit `null`, so this still needs its own fallback. + const resolvedSchema = schema || {} + const errorEl = document.getElementById('profile-error') + const fieldList = document.getElementById('profile-field-list') + const baseUrl = import.meta.env.VITE_THUNDERID_BASE_URL + const fetcher = createFetcher(auth) + + // Tracks the latest known attributes so successive per-field edits merge against + // up-to-date values without refetching the whole profile on every save. + let currentAttributes = { ...(profile?.attributes || {}) } + + const refreshHeader = () => { + const mergedUser = { ...user, ...currentAttributes } + const displayName = getDisplayName(mergedUser, currentAttributes) + + const avatarEl = document.querySelector('.profile-summary-avatar') + if (avatarEl) { + const avatar = renderAvatarInner(mergedUser, displayName) + avatarEl.className = `profile-summary-avatar ${avatar.className}` + avatarEl.setAttribute('style', avatar.style || '') + avatarEl.innerHTML = avatar.html + } + + const nameEl = document.querySelector('.profile-summary-name') + if (nameEl) nameEl.textContent = displayName + } + + const showError = (message) => { + if (!errorEl) return + errorEl.hidden = false + errorEl.textContent = message + } + const clearError = () => { + if (!errorEl) return + errorEl.hidden = true + errorEl.textContent = '' + } + + const startEdit = (row, key, schemaEntry) => { + const display = row.querySelector('.profile-field-row-display') + if (!display) return + const currentValue = currentAttributes[key] ?? '' + + display.innerHTML = ` + +
+ + +
` + + const input = display.querySelector('input') + input?.focus() + + const cancel = () => { + row.outerHTML = renderFieldRow(key, schemaEntry, currentAttributes[key]) + } + + const save = async () => { + const value = input?.value.trim() || '' + const label = fieldLabel(key, schemaEntry) + const fieldError = validateField(schemaEntry, label, value) + if (fieldError) { + showError(fieldError) + return + } + clearError() + + const saveBtn = display.querySelector('[data-action="save"]') + if (saveBtn) saveBtn.disabled = true + + try { + const mergedAttributes = deepMerge(currentAttributes, { [key]: value }) + const updatedUser = await updateMeProfile({ baseUrl, payload: mergedAttributes, fetcher }) + + currentAttributes = { ...currentAttributes, [key]: updatedUser?.[key] ?? value } + row.outerHTML = renderFieldRow(key, schemaEntry, currentAttributes[key]) + refreshHeader() + + onSaved?.({ ...user, ...currentAttributes }) + } catch (err) { + showError(err?.message || 'Failed to update profile. Please try again.') + if (saveBtn) saveBtn.disabled = false + } + } + + display.addEventListener('click', (e) => { + const action = e.target.closest('[data-action]')?.dataset.action + if (action === 'save') save() + if (action === 'cancel') cancel() + }) + + input?.addEventListener('keydown', (e) => { + if (e.key === 'Enter') save() + if (e.key === 'Escape') cancel() + }) + } + + fieldList?.addEventListener('click', (e) => { + const editBtn = e.target.closest('[data-action="edit"]') + if (!editBtn) return + const row = editBtn.closest('.profile-field-row') + const key = row?.dataset.field + if (!row || !key) return + startEdit(row, key, resolvedSchema[key]) + }) +} diff --git a/quickstarts/browser/src/main.js b/quickstarts/browser/src/main.js new file mode 100644 index 0000000..06eb46a --- /dev/null +++ b/quickstarts/browser/src/main.js @@ -0,0 +1,101 @@ +import './style.css' +import auth, { missingEnvVars } from './auth.js' +import { renderSignedOutNav, renderSignedInNav, attachNavHandlers, attachSignedOutNavHandlers } from './components/nav.js' +import { mountAccountPage } from './pages/account.js' +import { renderSignedOut, renderHome, renderConfigNeeded, startCountdown, attachSignedOutHandlers, attachConfigNeededHandlers } from './pages/home.js' +import { renderTokenDebug, attachTokenHandlers } from './pages/token.js' + +let isDark = false +let currentPage = 'home' +let user = null +let idToken = null +let rawToken = null +let timer = null + +function navigateTo(page) { + if (timer) { clearInterval(timer); timer = null } + currentPage = page + renderSignedInPage() +} + +function renderSignedInPage() { + const app = document.getElementById('app') + if (!app) return + + if (currentPage === 'account') { + app.innerHTML = renderSignedInNav({ user, isDark, currentPage }) + '
' + attachNavHandlers({ + isDark: () => isDark, + setIsDark: (v) => { isDark = v }, + navigateTo, + auth, + }) + mountAccountPage(document.getElementById('account-root'), { + user, + auth, + onUserUpdated: (updatedUser) => { + user = updatedUser + }, + }) + return + } + + let content + if (currentPage === 'token') { + content = renderTokenDebug({ rawToken }) + } else { + content = renderHome({ user, idToken }) + } + + app.innerHTML = renderSignedInNav({ user, isDark, currentPage }) + content + + attachNavHandlers({ + isDark: () => isDark, + setIsDark: (v) => { isDark = v }, + navigateTo, + auth, + }) + + if (currentPage === 'token') { + attachTokenHandlers({ rawToken }) + } else if (currentPage === 'home') { + timer = startCountdown({ idToken }) + } +} + +async function renderApp() { + const app = document.getElementById('app') + if (!app) return + + if (missingEnvVars.length > 0) { + app.innerHTML = renderSignedOutNav({ isDark, hideSignIn: true }) + renderConfigNeeded(missingEnvVars) + attachSignedOutNavHandlers({ auth }) + attachConfigNeededHandlers() + return + } + + if (timer) { clearInterval(timer); timer = null } + + await auth.signIn({ callOnlyOnRedirect: true }) + + const signedIn = await auth.isSignedIn() + + if (signedIn) { + user = await auth.getUser() + try { idToken = await auth.getDecodedIdToken() } catch { idToken = null } + try { rawToken = await auth.getAccessToken() } catch { rawToken = null } + currentPage = 'home' + renderSignedInPage() + } else { + user = null + idToken = null + rawToken = null + currentPage = 'home' + + app.innerHTML = renderSignedOutNav({ isDark }) + renderSignedOut() + attachSignedOutNavHandlers({ auth }) + attachSignedOutHandlers({ auth }) + } +} + +renderApp() diff --git a/quickstarts/browser/src/pages/account.js b/quickstarts/browser/src/pages/account.js new file mode 100644 index 0000000..3e2fcb3 --- /dev/null +++ b/quickstarts/browser/src/pages/account.js @@ -0,0 +1,307 @@ +import { + CredentialConstants, + evaluateChangePasswordForm, + mapCredentialUpdateError, + resolveChangeCredentialPolicy, + supportsCredential, + updateMeCredentials, +} from '@thunderid/browser' +import { createFetcher, escapeHtml, fetchProfileFormContext, renderProfileFields, attachProfileFieldHandlers } from '../components/profileFields.js' + +const ICON_HOME = `` +const ICON_PERSON = `` +const ICON_SHIELD = `` +const ICON_EYE = `` +const ICON_CHECK = `` +const ICON_X = `` +const ICON_CHEVRON = `` + +const TABS = [ + { id: 'home', label: 'Home', icon: ICON_HOME }, + { id: 'personal', label: 'Personal info', icon: ICON_PERSON }, + { id: 'security', label: 'Security', icon: ICON_SHIELD }, +] + +const TILES = [ + { id: 'personal', icon: ICON_PERSON, tone: 'blue', title: 'Personal info', description: 'Name, email, phone, and profile photo' }, + { id: 'security', icon: ICON_SHIELD, tone: 'green', title: 'Security', description: 'Password and other credentials' }, +] + +// Mirrors the React/Vue quickstarts' Security tab: one credential per row. +const CREDENTIALS = [ + { + attribute: CredentialConstants.PASSWORD, + title: 'Password', + description: 'Used to sign in to your account.', + cta: 'Change password', + submitLabel: 'Update Password', + }, +] + +function renderHomeTab(user) { + const givenName = user?.given_name || user?.givenName || user?.name || user?.username || 'there' + + const tiles = TILES.map( + (tile) => ` + `, + ).join('') + + return ` + + + ` +} + +function renderCredentialCard(credential, { schema, openAttribute }) { + const isOpen = openAttribute === credential.attribute + + return ` + ` +} + +// The form fields, always rendered so the "unavailable" state has something to sit behind +// (blurred, disabled) rather than showing nothing at all β€” matches +// `BaseChangeCredential`'s unavailable rendering in the React/Vue SDKs. +function renderCredentialFields(credential) { + const nameLower = credential.title.toLowerCase() + const newFieldId = `cred-${credential.attribute}-new` + const confirmFieldId = `cred-${credential.attribute}-confirm` + + return ` +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ + +
+ +
` +} + +function renderCredentialForm(credential, schema) { + const unavailable = !supportsCredential(schema, credential.attribute) + const fields = renderCredentialFields(credential) + + const body = unavailable + ? ` +
+
${fields}
+
+ ${escapeHtml(credential.title)} changes unavailable +
Please contact your administrator.
+
+
` + : `
${fields}
` + + return `` +} + +function renderSecurityTab({ schema, openAttribute }) { + const cards = CREDENTIALS.map((credential) => renderCredentialCard(credential, { schema, openAttribute })).join('') + + return ` + + + ` +} + +function renderPersonalTab(user, { schema, profile }) { + return ` + + + ` +} + +function renderAccountPage({ tab, user, schema, profile, openAttribute }) { + const nav = TABS.map( + (t) => ` + `, + ).join('') + + let content + if (tab === 'personal') { + content = renderPersonalTab(user, { schema, profile }) + } else if (tab === 'security') { + content = renderSecurityTab({ schema, openAttribute }) + } else { + content = renderHomeTab(user) + } + + return ` + ` +} + +function attachCredentialForm(formEl, { attribute, schema, baseUrl, auth, onDone }) { + const policy = resolveChangeCredentialPolicy(schema, attribute) + const newInput = formEl.querySelector('[data-cred-field="newValue"]') + const confirmInput = formEl.querySelector('[data-cred-field="confirmValue"]') + const reqBox = formEl.querySelector('[data-cred-requirements]') + const reqList = formEl.querySelector('[data-cred-requirements-list]') + const submitBtn = formEl.querySelector('[data-cred-submit]') + const errorBox = formEl.querySelector('[data-cred-error]') + const successBox = formEl.querySelector('[data-cred-success]') + + const evaluate = () => { + const { ruleResults, isValid } = evaluateChangePasswordForm( + { newPassword: newInput.value, confirmPassword: confirmInput.value }, + policy, + ) + + if (ruleResults.length > 0) { + reqBox.hidden = false + reqList.innerHTML = ruleResults + .map( + (result) => + `
  • ${result.passed ? ICON_CHECK : ICON_X} Matches the required format
  • `, + ) + .join('') + } else { + reqBox.hidden = true + } + + submitBtn.disabled = !isValid + } + + newInput.addEventListener('input', evaluate) + confirmInput.addEventListener('input', evaluate) + + formEl.querySelectorAll('[data-cred-toggle-visibility]').forEach((btn) => { + btn.addEventListener('click', () => { + const input = btn.previousElementSibling + input.type = input.type === 'password' ? 'text' : 'password' + }) + }) + + submitBtn.addEventListener('click', async () => { + errorBox.hidden = true + successBox.hidden = true + submitBtn.disabled = true + + try { + const fetcher = createFetcher(auth) + await updateMeCredentials({ baseUrl, payload: { [attribute]: newInput.value }, fetcher }) + + successBox.hidden = false + newInput.value = '' + confirmInput.value = '' + evaluate() + onDone?.() + } catch (err) { + const { message } = mapCredentialUpdateError(err) + errorBox.hidden = false + errorBox.textContent = message || 'An error occurred while updating your credential. Please try again.' + submitBtn.disabled = false + } + }) +} + +// Self-mounting Account page: fetches the schema/profile once, then owns its own tab and +// per-credential open/closed state, re-rendering just its own container on every change +// rather than the whole app shell. +export async function mountAccountPage(container, { user, auth, initialTab = 'home', onUserUpdated }) { + const baseUrl = import.meta.env.VITE_THUNDERID_BASE_URL + + let tab = initialTab + let openAttribute = null + let currentUser = user + + const { schema, profile } = await fetchProfileFormContext({ baseUrl, auth }) + + const render = () => { + container.innerHTML = renderAccountPage({ tab, user: currentUser, schema, profile, openAttribute }) + attachHandlers() + } + + const attachHandlers = () => { + container.querySelectorAll('[data-tab]').forEach((btn) => { + btn.addEventListener('click', () => { + tab = btn.dataset.tab + openAttribute = null + render() + }) + }) + + if (tab === 'personal') { + attachProfileFieldHandlers({ + user: currentUser, + auth, + schema, + profile, + onSaved: (updatedUser) => { + currentUser = updatedUser + onUserUpdated?.(updatedUser) + }, + }) + } + + if (tab === 'security') { + // The same button opens and closes the form: clicking it while its own credential is + // already open collapses it, same as clicking the chevron. + container.querySelectorAll('[data-cred-toggle]').forEach((btn) => { + btn.addEventListener('click', () => { + const attribute = btn.dataset.credToggle + openAttribute = openAttribute === attribute ? null : attribute + render() + }) + }) + + const formEl = container.querySelector('[data-cred-form]') + if (formEl) { + attachCredentialForm(formEl, { + attribute: formEl.dataset.credForm, + schema, + baseUrl, + auth, + onDone: () => { + openAttribute = null + render() + }, + }) + } + } + } + + render() +} diff --git a/quickstarts/browser/src/pages/home.js b/quickstarts/browser/src/pages/home.js new file mode 100644 index 0000000..24be3a5 --- /dev/null +++ b/quickstarts/browser/src/pages/home.js @@ -0,0 +1,321 @@ +const HERO_MARK = ` + + + +` + +const ICON_CLOCK = `` +const ICON_TIMER = `` +const ICON_BUILDING = `` +const ICON_ARROW = `` + +const NEXT_STEPS = [ + { + n: '01', + title: 'Explore use cases', + body: 'See what you can build β€” auth flows for web, mobile, APIs, and agents.', + cta: 'Browse use cases', + href: 'https://thunderid.dev/docs/next/use-cases/overview/', + }, + { + n: '02', + title: 'Learn about flows', + body: 'Understand how authorization code, PKCE, client credentials, and device flows work.', + cta: 'Read guide', + href: 'https://thunderid.dev/docs/next/guides/flows/what-are-flows/', + }, + { + n: '03', + title: 'Style your experience', + body: 'Customize the login UI, branding, and email templates to match your product.', + cta: 'Design guide', + href: 'https://thunderid.dev/docs/next/guides/design/overview/', + }, + { + n: '04', + title: 'Explore SDK APIs', + body: 'Full Browser SDK reference β€” client methods, PKCE configuration, and token management.', + cta: 'SDK reference', + href: 'https://thunderid.dev/docs/next/sdks/browser/overview/', + }, +] + +function escapeHtml(str) { + if (str == null) return '' + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +function getInitials(user) { + const given = user?.given_name || '' + const family = user?.family_name || '' + if (given && family) return (given[0] + family[0]).toUpperCase() + const name = user?.name || user?.username || user?.email || '?' + const parts = name.trim().split(/\s+/) + if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase() + return name.slice(0, 2).toUpperCase() +} + +function getAvatarUrl(user) { + return user?.profile || user?.profileUrl || user?.picture || user?.URL || null +} + +function renderAvatar(user, className) { + const avatarUrl = getAvatarUrl(user) + if (avatarUrl) { + return `
    ` + } + return `
    ${escapeHtml(getInitials(user))}
    ` +} + +function greeting(name) { + const h = new Date().getHours() + const tod = h < 12 ? 'morning' : h < 17 ? 'afternoon' : 'evening' + return `Good ${tod}, ${name}.` +} + +function formatTime(unixSeconds) { + if (!unixSeconds) return 'β€”' + return new Date(unixSeconds * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) +} + +export function formatCountdown(secsLeft) { + 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' } +} + +export function renderSignedOut() { + return ` +
    +
    +
    ${HERO_MARK}
    +
    + + Open source + +
    +

    Auth for Modern Apps and Agents

    +

    + ThunderID gives you OAuth 2.0, PKCE, MFA, and JWT out of the box. + Clone the Quickstart and ship auth before lunch. +

    +
    + +
    +
    +
    +
    + OAuth 2.0 + Authorization standard +
    +
    + < 5 min + Integration time +
    +
    + Apache 2.0 + License +
    +
    +
    +
    ` +} + +export function attachSignedOutHandlers({ auth }) { + document.getElementById('hero-sign-in-btn')?.addEventListener('click', () => auth.signIn()) +} + +export function renderConfigNeeded(missing) { + const origin = typeof window !== 'undefined' ? window.location.origin : '' + + return ` +
    +
    +
    ${HERO_MARK}
    +
    + + Setup required + +
    +

    Configuration needed

    +

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

    + +
    +
    Step 1 · Set environment variables
    +
      + ${missing.map(key => `
    • ${escapeHtml(key)}
    • `).join('')} +
    +

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

    +
    + +
    +
    Step 2 · Allow this origin for CORS
    +
    +

    + Sign-in requests from this origin will be blocked by the browser + until it's added to your ThunderID deployment's allowed CORS + origins. In the ThunderID Console, go to + Settings → CORS → Allowed origins and + add it. +

    +
    + ${escapeHtml(origin)} + +
    +
    +
    + +
    +
    Step 3 · Register redirect URIs
    +
    +

    + This origin also doubles as this app's Authorized redirect URI + and Post-Logout Redirect URI. In the ThunderID + Console, open this application and go to + Advanced Settings → OAuth2 Configuration, + then add it to both fields below. +

    +
    +
    +
    Authorized redirect URI
    +
    + ${escapeHtml(origin)} + +
    +
    +
    +
    Post-Logout Redirect URI
    +
    + ${escapeHtml(origin)} + +
    +
    +
    +
    +
    + +

    + Need more info? Take a look at the + Browser quickstart guide. +

    +
    +
    ` +} + +export function attachConfigNeededHandlers() { + if (!navigator.clipboard) return + document.querySelectorAll('.config-copy-btn').forEach((btn) => { + btn.addEventListener('click', () => { + navigator.clipboard.writeText(btn.getAttribute('data-copy') || '').then(() => { + const original = btn.textContent + btn.textContent = 'Copied!' + setTimeout(() => { btn.textContent = original }, 1500) + }) + }) + }) +} + +export function renderHome({ user, idToken }) { + const now = Math.floor(Date.now() / 1000) + const givenName = user?.given_name || user?.name || user?.username || 'there' + const email = user?.email || '' + + const authTime = idToken?.auth_time + const exp = idToken?.exp + const secsLeft = exp ? Math.max(0, exp - now) : null + const countdown = secsLeft !== null ? formatCountdown(secsLeft) : null + const orgName = escapeHtml(idToken?.org_name || idToken?.org_handle || 'Default') + + return ` +
    +
    + ${renderAvatar(user, 'home-avatar-initials')} +
    +

    ${escapeHtml(greeting(givenName))}

    +
    + ${email ? `${escapeHtml(email)}` : ''} + + + Session active + +
    +
    +
    + +
    +
    +
    ${ICON_CLOCK}
    +
    +
    ${formatTime(authTime)}
    +
    Signed in at
    +
    +
    +
    +
    ${ICON_TIMER}
    +
    +
    ${countdown ? countdown.text : 'β€”'}
    +
    Session expires in
    +
    +
    +
    +
    ${ICON_BUILDING}
    +
    +
    ${orgName}
    +
    Organisation
    +
    +
    +
    + +
    What's next
    + +
    ` +} + +export function startCountdown({ idToken }) { + const timer = setInterval(() => { + const el = document.getElementById('countdown-value') + if (!el) { clearInterval(timer); return } + const now = Math.floor(Date.now() / 1000) + const exp = idToken?.exp + const secsLeft = exp ? Math.max(0, exp - now) : null + if (secsLeft !== null) { + const cd = formatCountdown(secsLeft) + el.textContent = cd.text + el.style.color = cd.color + } + }, 1000) + return timer +} diff --git a/quickstarts/browser/src/pages/token.js b/quickstarts/browser/src/pages/token.js new file mode 100644 index 0000000..9c65551 --- /dev/null +++ b/quickstarts/browser/src/pages/token.js @@ -0,0 +1,118 @@ +const ICON_COPY = `` + +function escapeHtml(str) { + if (str == null) return '' + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +function decodeJwtPart(part) { + try { + const base64 = part.replace(/-/g, '+').replace(/_/g, '/') + const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, '=') + return JSON.parse(atob(padded)) + } catch { + return null + } +} + +export function renderTokenDebug({ rawToken }) { + const now = Math.floor(Date.now() / 1000) + const parts = rawToken ? rawToken.split('.') : [] + const jwtHeader = parts[0] ? decodeJwtPart(parts[0]) : null + const jwtPayload = parts[1] ? decodeJwtPart(parts[1]) : null + + const exp = jwtPayload?.exp + const expiresIn = exp ? exp - now : null + const expired = expiresIn !== null && expiresIn <= 0 + const minsLeft = expiresIn !== null ? Math.abs(Math.floor(expiresIn / 60)) : null + + const issuer = jwtPayload?.iss + const audience = Array.isArray(jwtPayload?.aud) ? jwtPayload.aud.join(', ') : jwtPayload?.aud + const scopes = jwtPayload?.scope + + return ` +
    +
    +
    +

    Token debug

    +

    Inspect your access token and decoded claims.

    +
    + ${expiresIn !== null ? ` +
    + + ${expired ? 'Expired' : `Valid Β· expires in ${minsLeft} min`} +
    ` : ''} +
    + + ${rawToken ? ` +
    +
    + + +
    + + ${escapeHtml(parts[0] || '')} + . + ${escapeHtml(parts[1] || '')} + . + ${escapeHtml(parts[2] || '')} + +
    ` : ` +
    +
    + +
    + Loading… +
    `} + +
    +
    +
    JWT Header
    +
    ${jwtHeader ? escapeHtml(JSON.stringify(jwtHeader, null, 2)) : '…'}
    +
    +
    +
    JWT Payload
    +
    ${jwtPayload ? escapeHtml(JSON.stringify(jwtPayload, null, 2)) : '…'}
    +
    +
    + + ${issuer || audience || scopes ? ` +
    + ${issuer ? ` +
    +
    Issuer
    +
    ${escapeHtml(issuer)}
    +
    ` : ''} + ${audience ? ` +
    +
    Audience
    +
    ${escapeHtml(audience)}
    +
    ` : ''} + ${scopes ? ` +
    +
    Scopes
    +
    ${escapeHtml(scopes)}
    +
    ` : ''} +
    ` : ''} +
    ` +} + +export function attachTokenHandlers({ rawToken }) { + document.getElementById('copy-btn')?.addEventListener('click', async () => { + if (!rawToken || !navigator.clipboard) return + await navigator.clipboard.writeText(rawToken) + const btn = document.getElementById('copy-btn') + if (btn) { + btn.innerHTML = `${ICON_COPY} Copied!` + setTimeout(() => { + const b = document.getElementById('copy-btn') + if (b) b.innerHTML = `${ICON_COPY} Copy` + }, 1500) + } + }) +} diff --git a/quickstarts/browser/src/style.css b/quickstarts/browser/src/style.css new file mode 100644 index 0000000..7b5ba13 --- /dev/null +++ b/quickstarts/browser/src/style.css @@ -0,0 +1,1595 @@ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + --bg: #F7F9FC; + --card: #ffffff; + --border: #DDE3EC; + --text: #05213F; + --muted: #5A7085; + --blue: #3688FF; + --blue-hover: #2270e8; + --blue-subtle: rgba(54, 136, 255, 0.08); + --secondary: #424242; + --shadow-sm: 0 1px 3px rgba(5, 33, 63, 0.08); + --shadow-md: 0 4px 16px rgba(5, 33, 63, 0.10); + --radius: 10px; + --radius-sm: 6px; +} + +.dark { + --bg: #080f1c; + --card: #0f1a2d; + --border: rgba(255, 255, 255, 0.09); + --text: #E0EAFF; + --muted: rgba(224, 234, 255, 0.48); + --blue-subtle: rgba(54, 136, 255, 0.14); + --secondary: #8b8b8b; + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4); +} + +html { + color-scheme: light; +} + +html.dark { + color-scheme: dark; +} + +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + background: var(--bg); + color: var(--text); + min-height: 100vh; + line-height: 1.5; + transition: background 0.2s ease, color 0.2s ease; +} + +/* Nav */ +.nav { + display: flex; + align-items: center; + gap: 12px; + padding: 0 28px; + height: 52px; + border-bottom: 1px solid var(--border); + background: rgba(247, 249, 252, 0.92); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + position: sticky; + top: 0; + z-index: 100; +} + +.dark .nav { + background: rgba(8, 15, 28, 0.92); +} + +.nav-logo { + display: flex; + align-items: center; + gap: 10px; + text-decoration: none; + color: inherit; + cursor: pointer; + flex: 1; +} + +.wordmark { + display: flex; + flex-direction: column; + gap: 1px; +} + +.wordmark-name { + font-size: 14px; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--text); + line-height: 1; +} + +.wordmark-sub { + font-size: 8.5px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--blue); + line-height: 1; +} + +.nav-actions { + display: flex; + align-items: center; + gap: 8px; +} + +/* Hero */ +.hero { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: 96px 32px 80px; + max-width: 560px; + margin: 0 auto; +} + +.hero-mark { + margin-bottom: 32px; + opacity: 0.9; +} + +.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(--muted); + margin-bottom: 28px; +} + +.hero-badge-line { + display: block; + width: 24px; + height: 1px; + background: var(--border); + flex-shrink: 0; +} + +.hero-title { + font-size: 42px; + font-weight: 700; + letter-spacing: -0.05em; + line-height: 1.1; + color: var(--text); + margin-bottom: 18px; +} + +.hero-subtitle { + font-size: 16px; + color: var(--muted); + line-height: 1.8; + letter-spacing: -0.01em; + max-width: 420px; + margin-bottom: 36px; +} + +.hero-ctas { + display: flex; + gap: 10px; + flex-wrap: wrap; + justify-content: center; + margin-bottom: 52px; +} + +.hero-divider { + width: 100%; + border: none; + border-top: 1px solid var(--border); + margin: 4px 0; +} + +.hero-stats { + width: 100%; + max-width: 480px; + display: grid; + grid-template-columns: repeat(3, 1fr); +} + +.stat { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 16px 8px; +} + +.stat:not(:last-child) { + border-right: 1px solid var(--border); +} + +.stat-value { + font-size: 15px; + font-weight: 700; + color: var(--text); +} + +.stat-label { + font-size: 12px; + color: var(--muted); +} + +/* Config notice */ +.config-badge { + color: #e88b3a; +} + +.config-list { + width: 100%; + max-width: 420px; + text-align: left; + list-style: none; + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + 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(--text); +} + +.config-list-item:not(:last-child) { + border-bottom: 1px solid var(--border); +} + +.config-hint { + font-size: 14px; + color: var(--muted); + line-height: 1.7; + max-width: 420px; +} + +.config-hint code { + background: var(--blue-subtle); + 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(--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(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px; +} + +.config-box-body { + font-size: 13px; + color: var(--muted); + line-height: 1.6; + margin-bottom: 14px; +} + +.config-value-row { + display: flex; + align-items: center; + gap: 8px; +} + +.config-value { + flex: 1; + min-width: 0; + overflow-x: auto; + white-space: nowrap; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 12px; + color: var(--text); + background: var(--blue-subtle); + border-radius: 4px; + padding: 8px 10px; +} + +.config-value-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--muted); + margin-bottom: 4px; +} + +.config-value-group { + display: flex; + flex-direction: column; + gap: 12px; +} + +.config-docs-note { + font-size: 13px; + color: var(--muted); + max-width: 420px; + margin: 0 auto; +} + +.config-docs-note a { + color: var(--blue); + font-weight: 600; +} + +/* Buttons */ +.btn-primary, +.btn-outline, +.btn-ghost { + display: inline-flex; + align-items: center; + justify-content: center; + height: 46px; + padding: 0 24px; + border-radius: 8px; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: opacity 0.15s ease, background 0.15s ease, border-color 0.15s ease; + white-space: nowrap; + text-decoration: none; + font-family: inherit; +} + +.btn-primary { + background: var(--blue); + color: #ffffff; + border: 1.5px solid var(--blue); +} + +.btn-primary:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn-primary:hover:not(:disabled) { + opacity: 0.88; +} + +.btn-outline { + background: transparent; + color: var(--blue); + border: 1.5px solid rgba(54, 136, 255, 0.4); +} + +.btn-outline:hover { + background: var(--blue-subtle); +} + +.btn-ghost { + background: transparent; + color: var(--muted); + border: none; +} + +.btn-ghost:hover { + background: var(--blue-subtle); + color: var(--text); +} + +.nav .btn-primary, +.nav .btn-outline, +.nav .btn-ghost { + height: 36px; + padding: 0 16px; + font-size: 14px; +} + +.dark-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + padding: 0; + background: transparent; + border: 1px solid var(--border); + border-radius: 8px; + color: var(--muted); + cursor: pointer; + transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease; + flex-shrink: 0; +} + +.dark-toggle:hover { + color: var(--text); + background: var(--border); +} + +/* User card */ +.user-card { + position: relative; + background: var(--card); + border: 1px solid var(--border); + border-radius: 16px; + padding: 40px 48px; + box-shadow: var(--shadow-md); + text-align: left; + min-width: 320px; + max-width: 460px; + width: 100%; + margin-top: 24px; +} + +.user-avatar { + width: 64px; + height: 64px; + border-radius: 50%; + background: var(--blue); + color: #ffffff; + display: flex; + align-items: center; + justify-content: center; + font-size: 22px; + font-weight: 700; + letter-spacing: -0.5px; + margin-bottom: 20px; + box-shadow: 0 4px 12px rgba(54, 136, 255, 0.3); +} + +.user-details { + display: flex; + flex-direction: column; + gap: 12px; +} + +.user-name { + font-size: 22px; + font-weight: 700; + letter-spacing: -0.4px; + color: var(--text); + margin-bottom: 4px; +} + +.user-field { + display: flex; + flex-direction: column; + gap: 2px; +} + +.user-field-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.07em; + color: var(--muted); +} + +.user-field-value { + font-size: 14px; + color: var(--text); + font-weight: 500; +} + +.user-badge { + display: inline-flex; + align-items: center; + gap: 5px; + margin-top: 24px; + font-size: 12px; + font-weight: 600; + color: #16a34a; + background: #f0fdf4; + border: 1px solid #bbf7d0; + padding: 4px 10px; + border-radius: 100px; +} + +.dark .user-badge { + color: #4ade80; + background: rgba(74, 222, 128, 0.1); + border-color: rgba(74, 222, 128, 0.25); +} + +/* Nav back button */ +.nav-back-btn { + display: inline-flex; + align-items: center; + gap: 4px; + height: 32px; + padding: 0 10px; + border-radius: 6px; + border: none; + background: transparent; + color: var(--muted); + font-size: 13px; + font-family: inherit; + letter-spacing: -0.01em; + cursor: pointer; + transition: background 0.15s ease, color 0.15s ease; +} + +.nav-back-btn:hover { + background: var(--border); + color: var(--text); +} + +/* User dropdown */ +.ud-wrapper { + position: relative; +} + +.ud-trigger { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 8px 4px 4px; + border-radius: 24px; + border: 1px solid var(--border); + background: transparent; + color: var(--text); + font-size: 13px; + font-weight: 600; + cursor: pointer; + font-family: inherit; + transition: background 0.15s ease, border-color 0.15s ease; +} + +.ud-trigger:hover { + background: var(--blue-subtle); + border-color: rgba(54, 136, 255, 0.3); +} + +.ud-avatar { + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--blue); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 700; + flex-shrink: 0; + overflow: hidden; +} + +.ud-avatar img { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; +} + +.ud-avatar--header { + width: 36px; + height: 36px; + font-size: 13px; +} + +.ud-name { + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ud-menu { + display: none; + position: absolute; + right: 0; + top: calc(100% + 6px); + min-width: 200px; + background: var(--card); + border: 1px solid var(--border); + border-radius: 10px; + box-shadow: var(--shadow-md); + overflow: hidden; + z-index: 200; +} + +.ud-menu.ud-menu--open { + display: block; +} + +.ud-header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 14px; +} + +.ud-header-name { + font-size: 13px; + font-weight: 600; + color: var(--text); +} + +.ud-header-email { + font-size: 12px; + color: var(--muted); + margin-top: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ud-divider { + height: 1px; + background: var(--border); +} + +.ud-item { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 10px 14px; + border: none; + background: transparent; + color: var(--text); + font-size: 13px; + font-weight: 500; + cursor: pointer; + text-align: left; + font-family: inherit; + transition: background 0.15s ease; +} + +.ud-item:hover { + background: var(--blue-subtle); +} + +/* Signed-in home page */ +.home-main { + width: 100%; + max-width: 900px; + margin: 0 auto; + padding: 52px 28px 80px; +} + +.home-avatar-initials { + width: 52px; + height: 52px; + border-radius: 50%; + background: var(--blue); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + font-weight: 700; + letter-spacing: -0.3px; + flex-shrink: 0; + box-shadow: 0 4px 12px rgba(54, 136, 255, 0.3); + overflow: hidden; +} + +.home-avatar-initials img { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; +} + +.home-greeting { + display: flex; + align-items: flex-start; + gap: 16px; + margin-bottom: 36px; +} + +.home-greeting-text { + min-width: 0; +} + +.home-greeting-name { + margin: 0 0 6px; + font-size: 26px; + font-weight: 700; + color: var(--text); + letter-spacing: -0.04em; +} + +.home-greeting-meta { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--muted); + letter-spacing: -0.01em; + flex-wrap: wrap; +} + +.home-dot { + display: inline-block; + width: 3px; + height: 3px; + border-radius: 50%; + background: var(--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; +} + +.home-stats { + display: flex; + gap: 0; + padding: 20px 0; + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--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(--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(--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(--muted); + text-transform: uppercase; + letter-spacing: 0.07em; + margin-top: 2px; +} + +.home-next-label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--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(--border); + text-decoration: none; + color: inherit; + cursor: pointer; + transition: background 0.2s ease; + 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(--text); + letter-spacing: -0.02em; + margin-bottom: 4px; +} + +.home-next-desc { + font-size: 13px; + color: var(--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 ease; + padding-top: 2px; +} + +/* Token debug page */ +.token-main { + width: 100%; + max-width: 780px; + margin: 0 auto; + padding: 52px 28px 80px; +} + +.token-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 36px; + flex-wrap: wrap; +} + +.token-title { + margin: 0 0 5px; + font-size: 22px; + font-weight: 700; + letter-spacing: -0.04em; + color: var(--text); +} + +.token-subtitle { + margin: 0; + font-size: 13px; + color: var(--muted); + letter-spacing: -0.01em; +} + +.token-badge { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 6px 12px; + border-radius: 6px; + font-size: 12px; + font-weight: 700; + letter-spacing: -0.01em; + flex-shrink: 0; +} + +.token-badge--expired { + background: rgba(220, 38, 38, 0.08); + border: 1px solid rgba(220, 38, 38, 0.22); + color: #dc2626; +} + +.token-badge--valid { + background: rgba(47, 189, 107, 0.08); + border: 1px solid rgba(47, 189, 107, 0.22); + color: #2fbd6b; +} + +.token-badge-dot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; + flex-shrink: 0; +} + +.token-raw-section { + margin-bottom: 20px; +} + +.token-raw-label-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.token-section-label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--muted); +} + +.token-copy-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 10px; + border-radius: 5px; + background: transparent; + border: 1px solid var(--border); + font-size: 11px; + font-weight: 600; + color: var(--muted); + cursor: pointer; + font-family: inherit; + letter-spacing: -0.01em; + transition: border-color 0.15s ease, color 0.15s ease; +} + +.token-copy-btn:hover { + border-color: #3688FF; + color: #3688FF; +} + +.token-raw { + display: block; + font-size: 11px; + font-family: monospace; + word-break: break-all; + line-height: 1.8; + background: #0b1120; + padding: 14px 16px; + border-radius: 8px; + overflow-x: auto; +} + +.token-raw--loading { + color: rgba(255, 255, 255, 0.3); +} + +.token-part--header { color: #ff7b72; } +.token-part--payload { color: #79c0ff; } +.token-part--signature { color: #3fb950; } +.token-dot { color: rgba(255, 255, 255, 0.2); } + +.token-decoded-grid { + display: grid; + grid-template-columns: 1fr 1.7fr; + gap: 12px; + margin-bottom: 20px; +} + +.token-code-box { + background: #0b1120; + border-radius: 10px; + overflow: hidden; +} + +.token-code-box-header { + padding: 9px 14px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.09em; + text-transform: uppercase; +} + +.token-code-box-header--header { + color: #ff7b72; + border-bottom: 1px solid rgba(255, 123, 114, 0.15); +} + +.token-code-box-header--payload { + color: #79c0ff; + border-bottom: 1px solid rgba(121, 192, 255, 0.15); +} + +.token-code-pre { + margin: 0; + padding: 14px; + font-size: 11.5px; + line-height: 1.8; + font-family: monospace; + color: #c9d1d9; + overflow-x: auto; + max-height: 220px; +} + +.token-meta-row { + display: flex; + gap: 32px; + padding-top: 20px; + border-top: 1px solid var(--border); + flex-wrap: wrap; +} + +.token-meta-label { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--muted); + margin-bottom: 5px; +} + +.token-meta-value { + font-size: 12px; + font-family: monospace; + color: var(--text); + letter-spacing: -0.01em; +} + +/* Personal info (account page) */ +.profile-summary { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; + margin-bottom: 12px; +} + +.profile-summary-avatar { + width: 70px; + height: 70px; + border-radius: 50%; + border: 1px solid var(--border); + background: var(--card); + color: var(--text); + display: flex; + align-items: center; + justify-content: center; + font-size: 28px; + font-weight: 600; + overflow: hidden; +} + +.profile-summary-avatar.has-gradient { + color: #fff; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + border: none; +} + +.profile-summary-avatar img { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; +} + +.profile-error { + font-size: 12px; + color: #dc2626; + background: rgba(220, 38, 38, 0.08); + border-radius: var(--radius-sm); + padding: 8px 10px; + margin-bottom: 14px; +} + +.profile-summary-name { + font-size: 24px; + font-weight: 600; + color: var(--text); +} + +.profile-summary-subtitle { + font-size: 14px; + color: var(--muted); +} + +.profile-field-list { + display: flex; + flex-direction: column; +} + +.profile-field-row { + display: flex; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid var(--border); +} + +.profile-field-row-label { + font-size: 14px; + font-weight: 500; + color: var(--muted); + width: 120px; + flex-shrink: 0; + line-height: 28px; +} + +.profile-field-row-display { + flex: 1; + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.profile-field-row-value { + flex: 1; + font-size: 14px; + color: var(--text); + line-height: 28px; + word-break: break-word; + text-align: left; +} + +.profile-field-row-value.placeholder { + font-style: italic; + opacity: 0.7; +} + +.profile-field-edit-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + flex-shrink: 0; + border: none; + background: transparent; + color: var(--muted); + border-radius: var(--radius-sm); + cursor: pointer; + opacity: 0.7; +} + +.profile-field-edit-btn:hover { + opacity: 1; + color: var(--blue); +} + +.profile-field-row-input { + flex: 1; + padding: 4px 8px; + font-size: 14px; + font-family: inherit; + color: var(--text); + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + min-width: 0; +} + +.profile-field-row-input:focus { + outline: none; + border-color: var(--blue); +} + +.profile-field-row-actions { + display: flex; + gap: 6px; +} + +.profile-field-btn { + display: inline-flex; + align-items: center; + justify-content: center; + height: 28px; + padding: 0 12px; + border: none; + border-radius: var(--radius-sm); + font-size: 12px; + font-weight: 600; + font-family: inherit; + cursor: pointer; + white-space: nowrap; + transition: opacity 0.15s ease; +} + +.profile-field-btn--save { + background: var(--blue); + color: #ffffff; +} + +.profile-field-btn--save:hover:not(:disabled) { + opacity: 0.9; +} + +.profile-field-btn--save:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.profile-field-btn--cancel { + background: var(--secondary); + color: #ffffff; +} + +.profile-field-btn--cancel:hover { + opacity: 0.9; +} + +/* Account page (Home / Personal info / Security, left-nav layout) */ +.account-page { + display: flex; + align-items: flex-start; + min-height: calc(100vh - 52px); +} + +.account-sidebar { + width: 280px; + flex-shrink: 0; + padding: 40px 24px; + position: sticky; + top: 52px; +} + +.account-sidebar-title { + font-size: 22px; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--text); + margin: 0 0 24px 16px; +} + +.account-nav { + display: flex; + flex-direction: column; + gap: 2px; +} + +.account-nav-item { + display: flex; + align-items: center; + gap: 14px; + width: 100%; + padding: 11px 16px; + border: none; + border-radius: 20px; + background: transparent; + color: var(--muted); + font-size: 14px; + font-weight: 500; + font-family: inherit; + text-align: left; + cursor: pointer; + white-space: nowrap; +} + +.account-nav-item:hover { + background: var(--border); + color: var(--text); +} + +.account-nav-item--active, +.account-nav-item--active:hover { + background: var(--blue-subtle); + color: var(--blue); +} + +.account-content { + flex: 1; + min-width: 0; + display: flex; + justify-content: center; + padding: 40px 32px 80px; + border-left: 1px solid var(--border); +} + +.account-content-inner { + width: 100%; + max-width: 720px; +} + +.account-content-title { + font-size: 26px; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--text); + margin: 0 0 6px; +} + +.account-content-subtitle { + font-size: 14px; + color: var(--muted); + margin: 0 0 32px; +} + +.account-tiles { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + margin-top: 8px; +} + +.account-tile { + text-align: left; + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 22px; + cursor: pointer; + font-family: inherit; + display: flex; + flex-direction: column; +} + +.account-tile:hover { + border-color: var(--blue); +} + +.account-tile-icon { + width: 36px; + height: 36px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 14px; +} + +.account-tile-icon--blue { + background: var(--blue-subtle); + color: var(--blue); +} + +.account-tile-icon--green { + background: rgba(16, 185, 129, 0.12); + color: #10b981; +} + +.account-tile-title { + font-size: 15px; + font-weight: 600; + color: var(--text); + margin-bottom: 4px; +} + +.account-tile-desc { + font-size: 12.5px; + color: var(--muted); + line-height: 1.5; +} + +/* Generic bordered content box, matching the Home tiles and Security cards. */ +.account-box { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 24px; +} + +/* Security tab: one credential per row, collapsed into an expandable form */ +.account-security-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.account-security-card { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 20px 22px; +} + +.account-security-card-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.account-security-card-title { + font-size: 14px; + font-weight: 600; + color: var(--text); + margin-bottom: 2px; +} + +.account-security-card-desc { + font-size: 12.5px; + color: var(--muted); +} + +.account-security-card-cta { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 6px; + padding: 0; + background: none; + border: none; + color: var(--blue); + font-size: 13px; + font-weight: 500; + font-family: inherit; + cursor: pointer; +} + +.account-security-card-cta:hover { + opacity: 0.8; +} + +.account-security-card-chevron { + display: inline-flex; + transition: transform 0.2s; +} + +.account-security-card-chevron--open { + transform: rotate(180deg); +} + +.account-security-card-form { + margin-top: 18px; + padding-top: 18px; + border-top: 1px solid var(--border); +} + +.account-security-card-error { + font-size: 12px; + color: #dc2626; + background: rgba(220, 38, 38, 0.08); + border-radius: var(--radius-sm); + padding: 8px 10px; + margin-bottom: 14px; +} + +.account-security-card-success { + font-size: 12px; + color: #059669; + background: rgba(5, 150, 105, 0.08); + border-radius: var(--radius-sm); + padding: 8px 10px; + margin-bottom: 14px; +} + +/* Change-credential form fields */ +.cred-field { + margin-bottom: 14px; +} + +.cred-label { + display: block; + font-size: 13px; + font-weight: 500; + color: var(--text); + margin-bottom: 6px; +} + +.cred-input-wrap { + position: relative; +} + +.cred-input { + width: 100%; + padding: 9px 38px 9px 12px; + font-size: 14px; + font-family: inherit; + color: var(--text); + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); +} + +.cred-input:focus { + outline: none; + border-color: var(--blue); +} + +.cred-eye-btn { + position: absolute; + top: 50%; + right: 8px; + transform: translateY(-50%); + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border: none; + background: transparent; + color: var(--muted); + cursor: pointer; +} + +.cred-requirements { + margin: -6px 0 14px; +} + +.cred-requirements-heading { + font-size: 12px; + color: var(--muted); + margin-bottom: 6px; +} + +.cred-requirements-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 4px; +} + +.cred-requirements-list li { + display: flex; + align-items: center; + gap: 8px; + font-size: 12.5px; + color: var(--muted); +} + +.cred-requirements-list li.passed { + color: #059669; +} + +.cred-requirements-list li svg { + flex-shrink: 0; +} + +.cred-form-actions { + display: flex; + justify-content: flex-end; +} + +.cred-unavailable { + position: relative; +} + +.cred-unavailable-content { + filter: blur(3px); + opacity: 0.55; + pointer-events: none; + user-select: none; +} + +.cred-unavailable-overlay { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + gap: 4px; + padding: 16px; + background: rgba(217, 119, 6, 0.08); + border: 1px solid rgba(217, 119, 6, 0.3); + border-radius: var(--radius-sm); + color: #d97706; + font-size: 13px; +} + +.cred-unavailable-overlay strong { + font-size: 13.5px; +} diff --git a/quickstarts/browser/vite.config.js b/quickstarts/browser/vite.config.js new file mode 100644 index 0000000..0515e7a --- /dev/null +++ b/quickstarts/browser/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + build: { + target: 'esnext', + }, +}) diff --git a/quickstarts/express/.env.example b/quickstarts/express/.env.example new file mode 100644 index 0000000..968abea --- /dev/null +++ b/quickstarts/express/.env.example @@ -0,0 +1,11 @@ +# Base URL of your ThunderID deployment. ex: https://localhost:8090 +THUNDERID_BASE_URL=https://localhost:8090 + +# OAuth2 Client ID for this app. ThunderID Console -> your application -> Overview. +THUNDERID_CLIENT_ID=your-client-id-here +# OAuth2 Client Secret for this app. Shown once when the application is created; +# if lost, regenerate it from the app's Credentials tab (under Edit). Server-only, never expose to the browser. +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/quickstarts/express/.gitignore b/quickstarts/express/.gitignore new file mode 100644 index 0000000..37d7e73 --- /dev/null +++ b/quickstarts/express/.gitignore @@ -0,0 +1,2 @@ +node_modules +.env diff --git a/quickstarts/express/.stackblitzrc b/quickstarts/express/.stackblitzrc new file mode 100644 index 0000000..5f0a200 --- /dev/null +++ b/quickstarts/express/.stackblitzrc @@ -0,0 +1,4 @@ +{ + "installDependencies": false, + "startCommand": "npm run prepare-dev && npm install && npm run dev" +} diff --git a/quickstarts/express/README.md b/quickstarts/express/README.md new file mode 100644 index 0000000..44da87a --- /dev/null +++ b/quickstarts/express/README.md @@ -0,0 +1,99 @@ +# ThunderID Express Quickstart + +Open in StackBlitz + +A minimal Express.js **API** protected by ThunderID access tokens, using the ThunderID JavaScript +SDK (`@thunderid/express`). Unlike the browser-focused quickstarts, this sample doesn't have a +sign-in button β€” it's a resource server. Open [http://localhost:3000](http://localhost:3000) and +you'll get API documentation (endpoints, curl examples, a downloadable Postman collection) instead +of a login screen. + +Protected routes validate the `Authorization: Bearer ` header against ThunderID's OIDC +`/oauth2/userinfo` endpoint β€” the same check any OAuth 2.0 resource server performs. A `/login` β†’ +`/token` helper is included purely so you have a fast way to mint a demo access token to test with. + +## Prerequisites + +- Node.js 18+ +- A running ThunderID instance (default: `https://localhost:8090`) +- A configured application with `http://localhost:3000/login` added as an authorized redirect URI + +## Getting started + +1. Copy the environment file and fill in your values: + ```sh + cp .env.example .env + ``` + +2. Edit `.env` with your application's credentials: + ``` + THUNDERID_CLIENT_ID= + THUNDERID_CLIENT_SECRET= + THUNDERID_BASE_URL=https://localhost:8090 + ``` + +3. Install dependencies and start the server: + ```sh + pnpm install + pnpm start + ``` + +Open [http://localhost:3000](http://localhost:3000) for the API documentation page. + +## Trying the API + +- `GET /api/public` β€” no auth required. +- `GET /api/protected`, `GET /api/me`, and `PATCH /api/me/credentials` β€” require `Authorization: Bearer `. + +To get a token to test with, open [http://localhost:3000/login](http://localhost:3000/login), +sign in once, and your access token (decoded claims included) will be shown at +[http://localhost:3000/token](http://localhost:3000/token). Then: + +```sh +curl http://localhost:3000/api/me \ + -H "Authorization: Bearer $ACCESS_TOKEN" +``` + +Prefer a GUI? Download the Postman collection from the API docs page (or directly at +`/postman-collection.json`), paste your token into the `accessToken` collection variable, and run +the requests from there. + +## Back-channel logout + +The demo session behind `/login` and `/token` can be ended by ThunderID itself. When the user's +ThunderID session ends somewhere else, for example by signing out of another application, +ThunderID posts a logout token to this app, and the SDK ends the local session. The route is one +line in `index.mjs`: + +```js +app.post('/backchannel-logout', handleBackchannelLogout()); +``` + +To try it: + +1. In the ThunderID Console, open this application, go to **Advanced Settings β†’ OAuth2 + Configuration**, and set **Back-Channel Logout URI** to + `http://localhost:3000/backchannel-logout`. +2. ThunderID refuses localhost and private network addresses by default. For local testing only, + turn that off in the server's `deployment.yaml` and restart it: + ```yaml + oauth: + logout: + backchannel: + reject_private_addresses: false + ``` +3. Sign in at [http://localhost:3000/login](http://localhost:3000/login), then sign in to a second + application in the same browser, so both share one ThunderID session. +4. Sign out of the second application. Open [http://localhost:3000/token](http://localhost:3000/token): + you are sent back to sign-in, because this app's session was ended too. + +The Bearer-token API routes (`/api/*`) are not affected: they check each access token with +ThunderID on every request and hold no session. + +The SDK keeps sessions in memory by default. If you run more than one instance of an app, give the +SDK a shared store, since ThunderID's request reaches only one of them. + +## Learn more + +- [ThunderID Docs](https://thunderid.dev/docs) +- [`@thunderid/express` SDK reference](https://thunderid.dev/docs/sdks/javascript/express) diff --git a/quickstarts/express/index.mjs b/quickstarts/express/index.mjs new file mode 100644 index 0000000..2b2c1b3 --- /dev/null +++ b/quickstarts/express/index.mjs @@ -0,0 +1,406 @@ +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import express from 'express'; +import cookieParser from 'cookie-parser'; +import { + thunderID, + handleBackchannelLogout, + handleSignIn, + handleSignOut, + protect, + updateMeCredentials, +} from '@thunderid/express'; +import {verifyBearerToken} from './lib/auth.mjs'; +import {layout, esc, escAttr, COPY_ICON} from './lib/layout.mjs'; +import {thunderMark} from './lib/thunderMark.mjs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const app = express(); +const port = 3000; +const baseUrl = process.env.THUNDERID_BASE_URL || 'https://localhost:8090'; + +const REQUIRED_ENV_VARS = ['THUNDERID_CLIENT_ID', 'THUNDERID_CLIENT_SECRET']; +const missingEnvVars = REQUIRED_ENV_VARS.filter((key) => !process.env[key]); + +app.use(cookieParser()); +app.use(express.json()); +app.use(express.static(path.join(__dirname, 'public'))); + +// The thunderID() middleware is only used here to power the `/login` and +// `/token` convenience routes, which exist so you have a fast way to obtain +// a real access token to try against the API below. The API routes +// themselves (`/api/*`) don't rely on cookies or sessions at all β€” see +// lib/auth.mjs. +if (missingEnvVars.length === 0) { + app.use( + thunderID({ + baseUrl, + clientId: process.env.THUNDERID_CLIENT_ID, + clientSecret: process.env.THUNDERID_CLIENT_SECRET, + // afterSignInUrl doubles as the OAuth2 redirect_uri sent to ThunderID, so + // it must match the redirect URI registered for this app (`/login`, + // where handleSignIn() below actually exchanges the code). Where the + // user lands once sign-in completes is controlled separately via + // onSignIn. + afterSignInUrl: 'http://localhost:3000/login', + // Same story as afterSignInUrl: this is the post_logout_redirect_uri + // sent to ThunderID, so it must match handleSignOut()'s mount path + // (`/logout`) for the `?state=sign_out_success` callback below to fire. + afterSignOutUrl: 'http://localhost:3000/logout', + onSignIn: (res) => res.redirect('/'), + onSignOut: (res) => res.redirect('/'), + }), + ); +} + +const requireBearer = verifyBearerToken(baseUrl); + +async function getSession(req) { + const client = req.thunderIDAuth; + const sessionId = client && req.cookies?.[client.getSessionCookieName()]; + if (!client || !sessionId) return {signedIn: false, accessToken: null, user: null}; + const signedIn = (await client.isSignedIn(sessionId)) ?? false; + if (!signedIn) return {signedIn: false, accessToken: null, user: null}; + const [accessToken, user] = await Promise.all([client.getAccessToken(sessionId), client.getUser(sessionId)]); + return {signedIn, accessToken, user}; +} + +// ── Configuration notice ──────────────────────────────────────────────── + +function renderConfigNeeded() { + return layout({ + title: 'Configuration needed', + body: `
    +
    +
    ${thunderMark(40)}
    +
    Setup required
    +

    Configuration needed

    +

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

    + +
    +
    Step 1 · Set environment variables
    +
      + ${missingEnvVars.map((key) => `
    • ${esc(key)}
    • `).join('')} +
    +

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

    +
    + +
    +
    Step 2 · Register redirect URIs
    +
    +

    Sign-in and sign-out are handled by this app's server, so no CORS + configuration is needed. In the ThunderID Console, open this application and go to + Advanced Settings → OAuth2 Configuration, then add the exact URIs below.

    +
    +
    +
    Authorized redirect URI
    + http://localhost:3000/login +
    +
    +
    Post-Logout Redirect URI
    + http://localhost:3000/logout +
    +
    +
    Back-Channel Logout URI (optional)
    + http://localhost:3000/backchannel-logout +
    +
    +
    +
    + +

    Need more info? Take a look at the + Express quickstart guide.

    +
    +
    `, + }); +} + +app.use((req, res, next) => { + if (missingEnvVars.length > 0 && req.path !== '/styles.css') { + return res.status(503).send(renderConfigNeeded()); + } + next(); +}); + +// ── API documentation / landing page ──────────────────────────────────── + +function curlBlock(lines) { + return `
    ${esc(lines.join('\n'))}
    `; +} + +/** Joins a multi-line, backslash-continued curl command into one pasteable line. */ +function toSingleLineCommand(lines) { + return lines + .map((line) => line.trim().replace(/\\$/, '').trim()) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); +} + +function copyButton(text) { + return ``; +} + +function endpoint({method, path: routePath, summary, protectedRoute, curl, sample}) { + return ` +
    + + ${esc(method)} + ${esc(routePath)} + ${esc(summary)} + ${protectedRoute ? 'Protected' : 'Public'} + +
    +
    + Request + ${copyButton(toSingleLineCommand(curl))} +
    + ${curlBlock(curl)} +
    Sample response
    +
    ${esc(sample)}
    +
    +
    `; +} + +function bearerLine(accessToken) { + return accessToken ? `Bearer ${accessToken}` : 'Bearer $ACCESS_TOKEN'; +} + +app.get('/', async (req, res) => { + const {signedIn, accessToken, user} = await getSession(req); + + const hero = ` +
    +
    +
    ${thunderMark(40)}
    +
    Open source
    +

    Auth for Modern Apps and Agents

    +

    ThunderID gives you OAuth 2.0, PKCE, MFA, and JWT out of + the box. Clone the Quickstart and ship auth before lunch.

    +
    +
    +
    OAuth 2.0Authorization standard
    +
    < 5 minIntegration time
    +
    Apache 2.0License
    +
    +
    +
    `; + + const authCard = signedIn + ? `
    +

    You're signed in. Here's your access token β€” it's already substituted into the curl + examples below, so you can copy and run them as-is.

    +
    + + ${copyButton(accessToken || '')} +
    + ${esc(accessToken)} +

    See the full decoded token, expiry, and claims at /token.

    +
    ` + : `
    + +
    ${curlBlock(['Authorization: Bearer '])}
    +

    Sign in above for a demo token, or obtain one via OAuth 2.0 in your own app.

    +
    `; + + const body = `${hero} +
    + + ${authCard} + + + ${endpoint({ + method: 'GET', + path: '/api/public', + summary: 'No authentication required.', + protectedRoute: false, + curl: ['curl http://localhost:3000/api/public'], + sample: JSON.stringify({message: 'This endpoint is public. No token needed.'}, null, 2), + })} + ${endpoint({ + method: 'GET', + path: '/api/protected', + summary: 'Returns a sample resource for the authenticated caller.', + protectedRoute: true, + curl: [ + 'curl http://localhost:3000/api/protected \\', + ` -H "Authorization: ${bearerLine(accessToken)}"`, + ], + sample: JSON.stringify( + {message: 'You are authenticated.', subject: ''}, + null, + 2, + ), + })} + ${endpoint({ + method: 'GET', + path: '/api/me', + summary: "Proxies ThunderID's userinfo claims for the token's owner.", + protectedRoute: true, + curl: ['curl http://localhost:3000/api/me \\', ` -H "Authorization: ${bearerLine(accessToken)}"`], + sample: JSON.stringify({sub: '...', email: 'jane@example.com', given_name: 'Jane'}, null, 2), + })} + ${endpoint({ + method: 'PATCH', + path: '/api/me/credentials', + summary: 'Updates credentials (such as password) for the authenticated caller.', + protectedRoute: true, + curl: [ + 'curl -X PATCH http://localhost:3000/api/me/credentials \\', + ` -H "Authorization: ${bearerLine(accessToken)}" \\`, + ' -H "Content-Type: application/json" \\', + ' -d \'{"password": "n3wP@ssword!"}\'', + ], + sample: '204 No Content', + })} + + +
    +

    Prefer a GUI? Download the collection below β€” it ships with requests for every + endpoint above and a {{accessToken}} variable you can fill in from + /token.

    + +
    +
    + `; + res.send(layout({title: 'API docs', signedIn, user, body})); +}); + +// ── Demo token helper (browser-based, cookie session) ─────────────────── + +app.get('/login', handleSignIn()); +app.get('/logout', handleSignOut()); + +// ThunderID posts a logout token here when the session behind the demo cookie ends somewhere +// else, such as a sign-out from another application. The SDK validates the token and ends the +// local session, so `/token` sends you back to sign-in. Register this path as the application's +// Back-Channel Logout URI to use it (see the README). +app.post('/backchannel-logout', handleBackchannelLogout()); + +app.get('/token', protect((res) => res.redirect('/login')), async (req, res) => { + const {accessToken, user} = await getSession(req); + + const parts = accessToken ? accessToken.split('.') : []; + const decodePart = (part) => { + try { + return JSON.parse(Buffer.from(part.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8')); + } catch { + return null; + } + }; + const header = parts[0] ? decodePart(parts[0]) : null; + const payload = parts[1] ? decodePart(parts[1]) : null; + const issuer = payload?.iss; + const audience = Array.isArray(payload?.aud) ? payload.aud.join(', ') : payload?.aud; + const scopes = payload?.scope; + + const body = `
    +
    +
    +

    Token debug

    +

    Inspect your access token and decoded claims.

    +
    +
    Valid
    +
    + +
    +
    + + ${copyButton(accessToken || '')} +
    + ${esc(parts[0])}.${esc(parts[1])}.${esc(parts[2])} +
    + +
    +
    +
    JWT Header
    +
    ${esc(JSON.stringify(header, null, 2))}
    +
    +
    +
    JWT Payload
    +
    ${esc(JSON.stringify(payload, null, 2))}
    +
    +
    + + ${ + issuer || audience || scopes + ? `
    + ${issuer ? `
    Issuer
    ${esc(issuer)}
    ` : ''} + ${audience ? `
    Audience
    ${esc(audience)}
    ` : ''} + ${scopes ? `
    Scopes
    ${esc(scopes)}
    ` : ''} +
    ` + : '' + } + + +
    `; + res.send(layout({title: 'Token debug', signedIn: true, showBack: true, user, body})); +}); + +// ── API routes (bearer-token protected, no cookies involved) ──────────── + +app.get('/api/public', (_req, res) => { + res.json({message: 'This endpoint is public. No token needed.'}); +}); + +app.get('/api/protected', requireBearer, (req, res) => { + res.json({ + message: 'You are authenticated.', + subject: req.thunderIDUserInfo.sub ?? req.thunderIDUserInfo.id, + }); +}); + +app.get('/api/me', requireBearer, (req, res) => { + res.json(req.thunderIDUserInfo); +}); + +app.patch('/api/me/credentials', requireBearer, async (req, res) => { + try { + const authHeader = req.headers.authorization; + const token = authHeader?.split(' ')[1]; + await updateMeCredentials({ + baseUrl, + headers: {Authorization: `Bearer ${token}`}, + payload: req.body, + }); + res.status(204).end(); + } catch (error) { + res.status(error?.statusCode || 500).json({ + error: error?.code || 'CREDENTIAL_UPDATE_FAILED', + message: error?.message || 'Failed to update credentials', + }); + } +}); + +// ── Postman collection download ───────────────────────────────────────── + +app.get('/postman-collection.json', (_req, res) => { + res.download(path.join(__dirname, 'postman', 'ThunderID-Express-Quickstart.postman_collection.json')); +}); + +app.listen(port, () => { + console.log(`Server running on http://localhost:${port}`); +}); diff --git a/quickstarts/express/lib/auth.mjs b/quickstarts/express/lib/auth.mjs new file mode 100644 index 0000000..61be3f3 --- /dev/null +++ b/quickstarts/express/lib/auth.mjs @@ -0,0 +1,47 @@ +/** + * Bearer-token protection for the API routes in this quickstart. + * + * This is a resource-server style check: it does not depend on a browser + * session or cookie. Any client (curl, Postman, another service) can call + * a protected endpoint by sending a ThunderID-issued access token in the + * `Authorization: Bearer ` header. The token is validated by asking + * ThunderID's OIDC userinfo endpoint whether it's still valid β€” the same + * endpoint any OAuth 2.0 resource server would call. + */ +export function verifyBearerToken(baseUrl) { + return async function bearerAuth(req, res, next) { + const header = req.headers.authorization || ''; + const [scheme, token] = header.split(' '); + + if (scheme !== 'Bearer' || !token) { + res.set('WWW-Authenticate', 'Bearer realm="thunderid-express-quickstart"'); + return res.status(401).json({ + error: 'unauthorized', + message: 'Missing or malformed Authorization header. Expected: Authorization: Bearer ', + }); + } + + let response; + try { + response = await fetch(`${baseUrl}/oauth2/userinfo`, { + headers: {Authorization: `Bearer ${token}`}, + }); + } catch { + return res.status(502).json({ + error: 'bad_gateway', + message: 'Could not reach ThunderID to validate the access token.', + }); + } + + if (!response.ok) { + res.set('WWW-Authenticate', 'Bearer error="invalid_token"'); + return res.status(401).json({ + error: 'invalid_token', + message: 'The access token is invalid or expired. Get a fresh one at /token.', + }); + } + + req.thunderIDUserInfo = await response.json(); + next(); + }; +} diff --git a/quickstarts/express/lib/expressLogo.mjs b/quickstarts/express/lib/expressLogo.mjs new file mode 100644 index 0000000..733e4bc --- /dev/null +++ b/quickstarts/express/lib/expressLogo.mjs @@ -0,0 +1,10 @@ +export function expressLogo(size = 24) { + return ` + + + + + + +`; +} diff --git a/quickstarts/express/lib/layout.mjs b/quickstarts/express/lib/layout.mjs new file mode 100644 index 0000000..9b4f97d --- /dev/null +++ b/quickstarts/express/lib/layout.mjs @@ -0,0 +1,96 @@ +import {expressLogo} from './expressLogo.mjs'; + +export function esc(value) { + if (value === null || value === undefined) return ''; + return String(value) + .replace(/&/g, '&') + .replace(//g, '>'); +} + +/** Safe for embedding inside a double-quoted HTML attribute. */ +export function escAttr(value) { + return esc(value).replace(/"/g, '"'); +} + +const MOON_ICON = ``; +const SUN_ICON = ``; +const CHEVRON_LEFT_ICON = ``; +const KEY_ICON = ``; +const SIGN_OUT_ICON = ``; +export const COPY_ICON = ``; + +function userMenu(user) { + const displayName = user?.name || [user?.given_name, user?.family_name].filter(Boolean).join(' ') || user?.username || user?.email || 'Account'; + const initial = displayName.charAt(0).toUpperCase(); + + return `
    + + ${esc(initial)} + ${esc(displayName)} + +
    +
    + ${esc(initial)} + ${esc(displayName)} +
    + ${KEY_ICON}Token debug + ${SIGN_OUT_ICON}Sign out +
    +
    `; +} + +function nav({signedIn, showBack, user}) { + const authAction = signedIn ? userMenu(user) : `Sign in`; + + return ``; +} + +/** + * Wraps page content in the shared ThunderID quickstart shell. Uses the same + * nav/hero/button classnames and design tokens as the React and Next.js + * quickstarts so the sample apps read as one family. + */ +export function layout({title, body, signedIn = false, showBack = false, user = null}) { + return ` + + + + +${esc(title)} Β· ThunderID Express Quickstart + + + + +
    + ${nav({signedIn, showBack, user})} + ${body} +
    + + +`; +} diff --git a/quickstarts/express/lib/thunderMark.mjs b/quickstarts/express/lib/thunderMark.mjs new file mode 100644 index 0000000..32c210d --- /dev/null +++ b/quickstarts/express/lib/thunderMark.mjs @@ -0,0 +1,8 @@ +export function thunderMark(height = 40) { + const width = Math.round((height / 257) * 207); + return ` + + + +`; +} diff --git a/quickstarts/express/package.json b/quickstarts/express/package.json new file mode 100644 index 0000000..03b98cc --- /dev/null +++ b/quickstarts/express/package.json @@ -0,0 +1,16 @@ +{ + "name": "@thunderid/express-quickstart", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "prepare-dev": "node scripts/prepare-dev.cjs", + "start": "node --env-file-if-exists=.env index.mjs", + "dev": "node --env-file-if-exists=.env --watch index.mjs" + }, + "dependencies": { + "@thunderid/express": "1.3.0", + "cookie-parser": "^1.4.7", + "express": "^4.21.2" + } +} diff --git a/quickstarts/express/pnpm-lock.yaml b/quickstarts/express/pnpm-lock.yaml new file mode 100644 index 0000000..a8ab4a8 --- /dev/null +++ b/quickstarts/express/pnpm-lock.yaml @@ -0,0 +1,760 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@thunderid/express': + specifier: 1.3.0 + version: 1.3.0(express@4.22.3) + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 + express: + specifier: ^4.21.2 + version: 4.22.3 + +packages: + + '@thunderid/express@1.3.0': + resolution: {integrity: sha512-CF+XvS3rRr891Vf3PVi+6iS20NfMg4F3j2hcrLe7GSXrnDzknwTT4t5SS7JsH0JNRozcCxoZA0ayFXzfrMh40Q==} + peerDependencies: + express: '>=4.21.2' + + '@thunderid/javascript@1.4.0': + resolution: {integrity: sha512-qHfIFzXOQTN1nhuBT91Tr8bLPvfyEDa7gUQSL7GNcJkcyyKfJ77meIwr91lCme0ySFUnTeKl0Bk+R8iIJiKXBw==} + + '@thunderid/node@1.3.0': + resolution: {integrity: sha512-hAFf4Bv3JZTcatF7Y/iarbd3lYLbIkz6SHzGKG5HOwh/a/Ip8qpu7u4npF6/0cNzrvZzY5g6awIymsIav75Xqw==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + + body-parser@1.20.8: + resolution: {integrity: sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + dom-walk@0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + express@4.22.3: + resolution: {integrity: sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==} + engines: {node: '>= 0.10.0'} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-random-values@3.0.0: + resolution: {integrity: sha512-mNznaBdYcpz7UAdnOtDGcLdNwAa79mXl5htEyyZ51YaeAWNf2g4x/2yCVBdNNTbi35wX0Stc2PJXM7G6rcONOA==} + engines: {node: 18 || >=20} + + global@4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + memory-cache@0.2.0: + resolution: {integrity: sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + min-document@2.19.2: + resolution: {integrity: sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@0.1.13: + resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + proxy-addr@2.0.8: + resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==} + engines: {node: '>= 0.10'} + + qs@6.16.1: + resolution: {integrity: sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + secure-random-bytes@5.0.1: + resolution: {integrity: sha512-48nvkahmdyoRbHW/x1HTGIk+i2/84MHFBB+rZ50cg1y/p2RjKG+fhTMQKZx4QD6T6/o2M2WHLPkncqQpjMMsUw==} + engines: {node: 18 || >=20} + + secure-random-octet@4.0.1: + resolution: {integrity: sha512-KEeg9wGi/96Q8Edp97gt5Z5dYCcvG7vPKdKFq6rPLCHtaaHwZKbhbC+gY74O7OjX8Fnels49wqc8IgkNhBJZfw==} + engines: {node: 18 || >=20} + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + +snapshots: + + '@thunderid/express@1.3.0(express@4.22.3)': + dependencies: + '@thunderid/node': 1.3.0 + express: 4.22.3 + uuid: 11.1.1 + transitivePeerDependencies: + - encoding + + '@thunderid/javascript@1.4.0': + dependencies: + jose: 6.2.3 + tslib: 2.8.1 + + '@thunderid/node@1.3.0': + dependencies: + '@thunderid/javascript': 1.4.0 + base64url: 3.0.1 + cross-fetch: 4.1.0 + fast-sha256: 1.3.0 + jose: 6.2.3 + memory-cache: 0.2.0 + secure-random-bytes: 5.0.1 + tslib: 2.8.1 + uuid: 11.1.1 + transitivePeerDependencies: + - encoding + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + array-flatten@1.1.1: {} + + base64url@3.0.1: {} + + body-parser@1.20.8: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.16.1 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + + cookie-signature@1.0.6: {} + + cookie-signature@1.0.7: {} + + cookie@0.7.2: {} + + cross-fetch@4.1.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + depd@2.0.0: {} + + destroy@1.2.0: {} + + dom-walk@0.1.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + express@4.22.3: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.8 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.13 + proxy-addr: 2.0.8 + qs: 6.16.1 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-sha256@1.3.0: {} + + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-random-values@3.0.0: + dependencies: + global: 4.4.0 + + global@4.4.0: + dependencies: + min-document: 2.19.2 + process: 0.11.10 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + jose@6.2.3: {} + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + memory-cache@0.2.0: {} + + merge-descriptors@1.0.3: {} + + methods@1.1.2: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + min-document@2.19.2: + dependencies: + dom-walk: 0.1.2 + + ms@2.0.0: {} + + ms@2.1.3: {} + + negotiator@0.6.3: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + parseurl@1.3.3: {} + + path-to-regexp@0.1.13: {} + + process@0.11.10: {} + + proxy-addr@2.0.8: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.16.1: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + range-parser@1.2.1: {} + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + secure-random-bytes@5.0.1: + dependencies: + secure-random-octet: 4.0.1 + + secure-random-octet@4.0.1: + dependencies: + get-random-values: 3.0.0 + + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + statuses@2.0.2: {} + + toidentifier@1.0.1: {} + + tr46@0.0.3: {} + + tslib@2.8.1: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + unpipe@1.0.0: {} + + utils-merge@1.0.1: {} + + uuid@11.1.1: {} + + vary@1.1.2: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 diff --git a/quickstarts/express/postman/ThunderID-Express-Quickstart.postman_collection.json b/quickstarts/express/postman/ThunderID-Express-Quickstart.postman_collection.json new file mode 100644 index 0000000..d2acf64 --- /dev/null +++ b/quickstarts/express/postman/ThunderID-Express-Quickstart.postman_collection.json @@ -0,0 +1,62 @@ +{ + "info": { + "name": "ThunderID Express Quickstart", + "description": "Sample requests for the ThunderID Express API quickstart. Public endpoints work as-is; protected endpoints need an `accessToken` collection variable β€” grab one from http://localhost:3000/token after signing in through the sample app, or run the OAuth 2.0 flow yourself against your ThunderID instance.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + {"key": "baseUrl", "value": "http://localhost:3000"}, + {"key": "thunderIdBaseUrl", "value": "https://localhost:8090"}, + {"key": "accessToken", "value": ""} + ], + "item": [ + { + "name": "Public endpoint", + "request": { + "method": "GET", + "header": [], + "url": {"raw": "{{baseUrl}}/api/public", "host": ["{{baseUrl}}"], "path": ["api", "public"]} + } + }, + { + "name": "Protected resource", + "request": { + "method": "GET", + "header": [{"key": "Authorization", "value": "Bearer {{accessToken}}"}], + "url": {"raw": "{{baseUrl}}/api/protected", "host": ["{{baseUrl}}"], "path": ["api", "protected"]} + } + }, + { + "name": "Current user (userinfo)", + "request": { + "method": "GET", + "header": [{"key": "Authorization", "value": "Bearer {{accessToken}}"}], + "url": {"raw": "{{baseUrl}}/api/me", "host": ["{{baseUrl}}"], "path": ["api", "me"]} + } + }, + { + "name": "Update credentials", + "request": { + "method": "PATCH", + "header": [ + {"key": "Authorization", "value": "Bearer {{accessToken}}"}, + {"key": "Content-Type", "value": "application/json"} + ], + "body": { + "mode": "raw", + "raw": "{\n \"password\": \"n3wP@ssword!\"\n}" + }, + "url": {"raw": "{{baseUrl}}/api/me/credentials", "host": ["{{baseUrl}}"], "path": ["api", "me", "credentials"]} + } + }, + { + "name": "ThunderID userinfo (direct)", + "request": { + "method": "GET", + "header": [{"key": "Authorization", "value": "Bearer {{accessToken}}"}], + "url": {"raw": "{{thunderIdBaseUrl}}/oauth2/userinfo", "host": ["{{thunderIdBaseUrl}}"], "path": ["oauth2", "userinfo"]}, + "description": "Calls ThunderID directly, bypassing the sample app, to show what /api/me is proxying." + } + } + ] +} diff --git a/quickstarts/express/public/styles.css b/quickstarts/express/public/styles.css new file mode 100644 index 0000000..26fffc1 --- /dev/null +++ b/quickstarts/express/public/styles.css @@ -0,0 +1,991 @@ +/* ── Reset ── */ +*, +::before, +::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +/* ── Design tokens (shared with the ThunderID React / Next.js quickstarts) ── */ +: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; + --color-code-bg: #0b1120; + --nav-height: 52px; + --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; + --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace; + --radius-card: 12px; + --radius-btn: 8px; + --transition: 0.2s ease; +} + +.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); +} + +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; +} + +a { + color: var(--color-primary); +} + +.app { + min-height: 100vh; + background: var(--color-bg); + color: var(--color-text); + transition: background var(--transition), color var(--transition); +} + +/* ── Nav ── */ +.nav { + position: fixed; + top: 0; + left: 0; + right: 0; + height: var(--nav-height); + display: flex; + align-items: center; + 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; + gap: 12px; +} + +.nav-logo { + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + text-decoration: none; + color: inherit; +} + +.wordmark-name { + font-size: 14px; + font-weight: 700; + letter-spacing: -0.03em; + line-height: 1.1; + color: var(--color-text); +} + +.nav-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.nav-back-btn { + display: inline-flex; + align-items: center; + gap: 4px; + height: 32px; + padding: 0 10px; + border-radius: 6px; + color: var(--color-muted); + font-size: 13px; + text-decoration: none; +} + +.nav-back-btn:hover { + background: var(--color-border); + color: var(--color-text); +} + +/* ── Dark-mode toggle ── */ +.dark-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: 8px; + border: 1px solid var(--color-border); + background: transparent; + color: var(--color-muted); + cursor: pointer; + flex-shrink: 0; + transition: border-color var(--transition), color var(--transition), background var(--transition); +} + +.dark-toggle:hover { + background: var(--color-border); + color: var(--color-text); +} + +.dark-toggle .icon-sun { + display: none; +} + +.dark .dark-toggle .icon-moon { + display: none; +} + +.dark .dark-toggle .icon-sun { + display: flex; +} + +/* ── Buttons ── */ +button { + font-family: var(--font-sans); +} + +.btn-primary, +.btn-outline, +.btn-ghost { + display: inline-flex; + align-items: center; + justify-content: center; + height: 46px; + padding: 0 24px; + border-radius: var(--radius-btn); + font-size: 15px; + font-weight: 600; + cursor: pointer; + border: none; + transition: opacity var(--transition), background var(--transition), border-color var(--transition); + white-space: nowrap; + text-decoration: none; + font-family: inherit; +} + +.btn-primary { + background: var(--color-primary); + color: #fff; + border: 1.5px solid var(--color-primary); +} + +.btn-primary:hover { + opacity: 0.88; +} + +.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); +} + +.btn-ghost { + background: transparent; + color: var(--color-muted); +} + +.btn-ghost:hover { + background: rgba(0, 0, 0, 0.04); +} + +.nav .btn-primary, +.nav .btn-outline, +.nav .btn-ghost { + height: 36px; + padding: 0 16px; + font-size: 14px; +} + +/* ── User menu (signed-in nav) ── */ +.user-menu { + position: relative; +} + +.user-menu-trigger { + display: flex; + align-items: center; + gap: 8px; + height: 36px; + padding: 0 12px 0 6px; + border-radius: 999px; + background: transparent; + border: none; + cursor: pointer; + list-style: none; + color: var(--color-text); + font-size: 13px; + font-weight: 600; +} + +.user-menu-trigger:hover { + background: var(--color-border); +} + +.user-menu-trigger::-webkit-details-marker { + display: none; +} + +.user-menu-trigger:focus, +.user-menu-trigger:focus-visible { + outline: none; +} + +.user-avatar-sm { + width: 26px; + height: 26px; + border-radius: 50%; + background: var(--color-primary); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; + flex-shrink: 0; +} + +.user-avatar-lg { + width: 40px; + height: 40px; + border-radius: 50%; + background: var(--color-primary); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: 700; + flex-shrink: 0; +} + +.user-menu-list { + position: absolute; + top: calc(100% + 8px); + right: 0; + min-width: 220px; + background: var(--color-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-card); + box-shadow: var(--shadow-card); + overflow: hidden; + z-index: 200; +} + +.user-menu-header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; +} + +.user-menu-header-name { + font-size: 14px; + font-weight: 700; + color: var(--color-text); + letter-spacing: -0.02em; +} + +.user-menu-item { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + font-size: 13.5px; + font-weight: 600; + color: var(--color-text); + text-decoration: none; + border-top: 1px solid var(--color-border); +} + +.user-menu-item:hover { + background: var(--color-bg); +} + +/* ── Hero ── */ +.hero { + margin-top: var(--nav-height); + padding: 88px 32px 56px; +} + +.hero-inner { + width: 100%; + max-width: 560px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.hero-mark { + display: flex; + justify-content: center; + margin-bottom: 32px; + color: var(--color-primary); +} + +.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-muted); + margin-bottom: 28px; +} + +.hero-badge-line { + display: block; + height: 1px; + width: 24px; + background: var(--color-border); + flex-shrink: 0; +} + +.config-badge { + color: #e88b3a; +} + +.hero-title { + font-size: 42px; + font-weight: 700; + letter-spacing: -0.05em; + line-height: 1.1; + color: var(--color-text); + margin: 0 0 18px; +} + +.hero-subtitle { + font-size: 16px; + color: var(--color-muted); + max-width: 440px; + line-height: 1.8; + letter-spacing: -0.01em; + margin: 0 auto 36px; +} + +.hero-divider { + width: 100%; + border: none; + border-top: 1px solid var(--color-border); + margin: 4px 0; +} + +.hero-stats { + width: 100%; + display: grid; + grid-template-columns: repeat(3, 1fr); +} + +.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); +} + +.hero-stat-label { + font-size: 12px; + color: var(--color-muted); +} + +/* ── Content shell (non-hero pages / sections below the hero) ── */ +.page { + width: 100%; + max-width: 780px; + margin: 0 auto; + margin-top: var(--nav-height); + padding: 40px 28px 96px; +} + +.docs-shell { + width: 100%; + max-width: 780px; + margin: 0 auto; + padding: 0 28px 96px; +} + +.eyebrow { + 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: 16px; +} + +.eyebrow-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: #2fbd6b; + flex-shrink: 0; +} + +.page-title { + font-size: 28px; + font-weight: 700; + letter-spacing: -0.04em; + line-height: 1.15; + margin-bottom: 12px; +} + +.page-subtitle { + font-size: 15px; + color: var(--color-text-muted); + max-width: 560px; + line-height: 1.7; + margin-bottom: 32px; +} + +.section-label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--color-muted); + margin: 40px 0 14px; +} + +.section-label:first-of-type { + margin-top: 0; +} + +/* ── Cards ── */ +.card { + background: var(--color-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-card); + box-shadow: var(--shadow-card); + padding: 24px; +} + +.card + .card { + margin-top: 14px; +} + +.card p { + font-size: 13.5px; + color: var(--color-text-muted); + line-height: 1.7; +} + +.card p + p { + margin-top: 10px; +} + +.card p code { + background: rgba(54, 136, 255, 0.1); + border-radius: 4px; + padding: 2px 6px; + font-family: var(--font-mono); + font-size: 12.5px; + color: var(--color-text); +} + +/* ── Endpoint list (swagger-like) ── */ +.endpoint { + border: 1px solid var(--color-border); + border-radius: var(--radius-card); + background: var(--color-card); + overflow: hidden; +} + +.endpoint + .endpoint { + margin-top: 12px; +} + +.endpoint-summary { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + cursor: pointer; + list-style: none; +} + +.endpoint-summary::-webkit-details-marker { + display: none; +} + +.method-badge { + flex-shrink: 0; + font-family: var(--font-mono); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.03em; + padding: 4px 9px; + border-radius: 5px; + color: #fff; + background: #2fbd6b; +} + +.endpoint-path { + font-family: var(--font-mono); + font-size: 14px; + font-weight: 600; + color: var(--color-text); +} + +.endpoint-desc { + font-size: 13px; + color: var(--color-text-muted); + flex: 1; + min-width: 0; +} + +.tag { + flex-shrink: 0; + font-size: 10.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + padding: 3px 9px; + border-radius: 999px; +} + +.tag-public { + color: #2fbd6b; + background: rgba(47, 189, 107, 0.12); +} + +.tag-protected { + color: #e88b3a; + background: rgba(232, 139, 58, 0.12); +} + +.endpoint-body { + padding: 0 20px 20px; + border-top: 1px solid var(--color-border); +} + +.endpoint-body-label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--color-muted); + margin: 16px 0 8px; +} + +/* ── Code blocks ── */ +.code-block { + position: relative; + background: var(--color-code-bg); + border-radius: 8px; + padding: 14px 16px; + overflow-x: auto; +} + +.code-block pre, +.code-block code { + font-family: var(--font-mono); + font-size: 12.5px; + line-height: 1.7; + color: #c9d1d9; + background: none; + padding: 0; + white-space: pre; +} + +/* ── Config notice ── */ +.config-list { + 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 24px; + width: 100%; + max-width: 420px; +} + +.config-list-item { + padding: 10px 0; + font-family: var(--font-mono); + font-size: 13px; +} + +.config-list-item:not(:last-child) { + border-bottom: 1px solid var(--color-border); +} + +.config-hint { + font-size: 14px; + color: var(--color-muted); + line-height: 1.7; + max-width: 420px; + margin: 0 auto; +} + +.config-hint code { + background: rgba(54, 136, 255, 0.08); + border-radius: 4px; + padding: 2px 6px; + font-family: var(--font-mono); + 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-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-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-muted); + margin-bottom: 4px; +} + +.config-value { + display: block; + overflow-x: auto; + white-space: nowrap; + font-family: var(--font-mono); + font-size: 12px; + color: var(--color-text); + background: rgba(54, 136, 255, 0.08); + border-radius: 4px; + padding: 8px 10px; +} + +.config-docs-note { + font-size: 13px; + color: var(--color-muted); + max-width: 420px; + margin: 0 auto; +} + +.config-docs-note a { + color: var(--color-primary); + font-weight: 600; +} + +/* ── Token debug page ── */ +.token-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 36px; + flex-wrap: wrap; +} + +.token-title { + margin: 0 0 5px; + font-size: 22px; + font-weight: 700; + letter-spacing: -0.04em; + color: var(--color-text); +} + +.token-subtitle { + margin: 0; + font-size: 13px; + color: var(--color-muted); + letter-spacing: -0.01em; +} + +.token-badge { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 6px 12px; + border-radius: 6px; + font-size: 12px; + font-weight: 700; + letter-spacing: -0.01em; + flex-shrink: 0; + background: rgba(47, 189, 107, 0.08); + border: 1px solid rgba(47, 189, 107, 0.22); + color: #2fbd6b; +} + +.token-badge--expired { + background: rgba(220, 38, 38, 0.08); + border: 1px solid rgba(220, 38, 38, 0.22); + color: #dc2626; +} + +.token-badge-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; + flex-shrink: 0; +} + +.token-raw-section { + margin-bottom: 20px; +} + +.token-raw-label-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.token-section-label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--color-muted); +} + +.token-copy-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 10px; + border-radius: 5px; + background: transparent; + border: 1px solid var(--color-border); + font-size: 11px; + font-weight: 600; + color: var(--color-muted); + cursor: pointer; + font-family: var(--font-sans); + letter-spacing: -0.01em; + transition: border-color 0.2s, color 0.2s; +} + +.token-copy-btn:hover { + border-color: var(--color-primary); + color: var(--color-primary); +} + +.token-raw { + display: block; + font-size: 11px; + font-family: var(--font-mono); + word-break: break-all; + line-height: 1.8; + color: #c9d1d9; + background: var(--color-code-bg); + padding: 14px 16px; + border-radius: 8px; + overflow-x: auto; +} + +.token-part--header { + color: #ff7b72; +} + +.token-part--payload { + color: #79c0ff; +} + +.token-part--signature { + color: #3fb950; +} + +.token-dot { + color: rgba(255, 255, 255, 0.2); +} + +.token-decoded-grid { + display: grid; + grid-template-columns: 1fr 1.7fr; + gap: 12px; + margin-bottom: 20px; +} + +.token-code-box { + background: var(--color-code-bg); + border-radius: 10px; + overflow: hidden; +} + +.token-code-box-header { + padding: 9px 14px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.09em; + text-transform: uppercase; +} + +.token-code-box-header--header { + color: #ff7b72; + border-bottom: 1px solid rgba(255, 123, 114, 0.15); +} + +.token-code-box-header--payload { + color: #79c0ff; + border-bottom: 1px solid rgba(121, 192, 255, 0.15); +} + +.token-code-pre { + margin: 0; + padding: 14px; + font-size: 11.5px; + line-height: 1.8; + font-family: var(--font-mono); + color: #c9d1d9; + overflow-x: auto; + max-height: 220px; +} + +.token-meta-row { + display: flex; + gap: 32px; + padding-top: 20px; + border-top: 1px solid var(--color-border); + flex-wrap: wrap; +} + +.token-meta-label { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--color-muted); + margin-bottom: 5px; +} + +.token-meta-value { + font-size: 12px; + font-family: var(--font-mono); + color: var(--color-text); + letter-spacing: -0.01em; +} + +/* ── Profile table ── */ +.kv-table { + width: 100%; + border-collapse: collapse; +} + +.kv-table th, +.kv-table td { + padding: 12px 0; + text-align: left; + font-size: 13.5px; + border-bottom: 1px solid var(--color-border); +} + +.kv-table th { + width: 140px; + font-weight: 600; + color: var(--color-muted); + text-transform: uppercase; + font-size: 11px; + letter-spacing: 0.06em; +} + +.kv-table td { + font-family: var(--font-mono); + word-break: break-all; +} + +.footer-note { + margin-top: 48px; + padding-top: 24px; + border-top: 1px solid var(--color-border); + font-size: 12.5px; + color: var(--color-muted); + display: flex; + gap: 20px; + flex-wrap: wrap; +} + +@media (max-width: 640px) { + .token-decoded-grid { + grid-template-columns: 1fr; + } + + .endpoint-summary { + flex-wrap: wrap; + } + + .hero-title { + font-size: 32px; + } +} diff --git a/quickstarts/express/scripts/prepare-dev.cjs b/quickstarts/express/scripts/prepare-dev.cjs new file mode 100644 index 0000000..734e095 --- /dev/null +++ b/quickstarts/express/scripts/prepare-dev.cjs @@ -0,0 +1,19 @@ +// 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'); +if (fs.existsSync(envExample) && !fs.existsSync(envTarget)) { + // 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. + const envContent = fs + .readFileSync(envExample, 'utf8') + .replace(/^([A-Z0-9_]+)=(your-\S*|generate-with-\S*|https:\/\/localhost:8090)$/gm, '$1='); + fs.writeFileSync(envTarget, envContent); +} diff --git a/quickstarts/flutter/.env.example b/quickstarts/flutter/.env.example new file mode 100644 index 0000000..4c5f308 --- /dev/null +++ b/quickstarts/flutter/.env.example @@ -0,0 +1,4 @@ +THUNDERID_BASE_URL=https://localhost:8090 +THUNDERID_APP_ID=your-application-id +THUNDERID_ATTESTATION_ENABLED=false +THUNDERID_CLOUD_PROJECT_NUMBER= diff --git a/quickstarts/flutter/.gitignore b/quickstarts/flutter/.gitignore new file mode 100644 index 0000000..48a41e5 --- /dev/null +++ b/quickstarts/flutter/.gitignore @@ -0,0 +1,130 @@ +# Miscellaneous +*.class +*.lock +*.log +*.pyc +*.swp +.buildlog/ +.history + +# Local ThunderID dev server certificate (machine-specific, see Flutter Quickstart docs) +server.cert +server.key + +# Flutter repo-specific +/bin/cache/ +/bin/internal/bootstrap.bat +/bin/internal/bootstrap.sh +/bin/mingit/ +/dev/benchmarks/mega_gallery/ +/dev/bots/.recipe_deps +/dev/bots/android_tools/ +/dev/devicelab/ABresults*.json +/dev/docs/doc/ +/dev/docs/flutter.docs.zip +/dev/docs/lib/ +/dev/docs/pubspec.yaml +/dev/integration_tests/**/xcuserdata +/dev/integration_tests/**/Pods +/packages/flutter/coverage/ +version +analysis_benchmark.json + +# packages file containing multi-root paths +.packages.generated + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +**/generated_plugin_registrant.dart +.packages +.pub-preload-cache/ +.pub/ +build/ +flutter_*.png +linked_*.ds +unlinked.ds +unlinked_spec.ds + +# Android related +**/android/**/gradle-wrapper.jar +.gradle/ +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/key.properties +*.jks + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/.last_build_id +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/ephemeral +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# macOS +**/Flutter/ephemeral/ +**/Pods/ +**/macos/Flutter/GeneratedPluginRegistrant.swift +**/macos/Flutter/ephemeral +**/xcuserdata/ + +# Windows +**/windows/flutter/generated_plugin_registrant.cc +**/windows/flutter/generated_plugin_registrant.h +**/windows/flutter/generated_plugins.cmake + +# Linux +**/linux/flutter/generated_plugin_registrant.cc +**/linux/flutter/generated_plugin_registrant.h +**/linux/flutter/generated_plugins.cmake + +# Coverage +coverage/ + +# Symbols +app.*.symbols + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages +!/dev/ci/**/Gemfile.lock + +# Log/OS Files +*.log +.DS_Store + +# Environment variables (use .env.example as a template) +.env +.env.local +.env.*.local diff --git a/quickstarts/flutter/.metadata b/quickstarts/flutter/.metadata new file mode 100644 index 0000000..0b1aaad --- /dev/null +++ b/quickstarts/flutter/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "cc0734ac716fbb8b90f3f9db8020958b1553afa7" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7 + base_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7 + - platform: android + create_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7 + base_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/quickstarts/flutter/README.md b/quickstarts/flutter/README.md new file mode 100644 index 0000000..44230be --- /dev/null +++ b/quickstarts/flutter/README.md @@ -0,0 +1,44 @@ +# ThunderID Flutter Quickstart + +ThunderID Flutter Quickstart demonstrates the full authentication lifecycle using the [`thunderid_flutter`](https://pub.dev/packages/thunderid_flutter) SDK on iOS and Android. + +**Flow demonstrated:** +1. App opens β†’ unauthenticated state (sign-in screen) +2. User initiates sign-in / sign-up β†’ SDK starts app-native Flow Execution +3. User completes the flow +4. Sign-in β†’ authenticated state with profile information, token debugging, and sign-out button. + Sign-up creates the account but does not start a session, so it returns to the landing screen + and the new credentials have to be used to sign in. +5. User taps Sign Out β†’ session terminated, returns to sign-in screen + +## Get the Sample + +```bash +git clone https://github.com/thunder-id/samples.git +cd samples/quickstarts/flutter +``` + +## Configure + +```bash +cp .env.example .env +``` + +| Variable | Description | +|---|---| +| `THUNDERID_BASE_URL` | Base URL of your ThunderID server (default `https://localhost:8090`). | +| `THUNDERID_APP_ID` | ID of the ThunderID application the sample signs in to. | +| `THUNDERID_ATTESTATION_ENABLED` | Set to `true` to enable platform attestation. | +| `THUNDERID_CLOUD_PROJECT_NUMBER` | Google Cloud project number, used for Play Integrity attestation on Android. | + +## Run + +```bash +flutter pub get +flutter run +``` + +See [Try the Flutter Sample App](https://thunderid.dev/docs/v1.0.x/sdks/flutter/guides/try-the-sample-app) +in the Flutter SDK docs for prerequisites, creating the application, attestation, passkeys, and run instructions. + +The SDK source lives in [thunder-id/flutter-sdks](https://github.com/thunder-id/flutter-sdks). diff --git a/quickstarts/flutter/analysis_options.yaml b/quickstarts/flutter/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/quickstarts/flutter/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/quickstarts/flutter/android/.gitignore b/quickstarts/flutter/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/quickstarts/flutter/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/quickstarts/flutter/android/app/build.gradle.kts b/quickstarts/flutter/android/app/build.gradle.kts new file mode 100644 index 0000000..55d73d4 --- /dev/null +++ b/quickstarts/flutter/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "dev.thunderid.flutter_quickstart" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "dev.thunderid.flutter_quickstart" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = 26 + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/quickstarts/flutter/android/app/src/debug/AndroidManifest.xml b/quickstarts/flutter/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/quickstarts/flutter/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/quickstarts/flutter/android/app/src/main/AndroidManifest.xml b/quickstarts/flutter/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bd098c0 --- /dev/null +++ b/quickstarts/flutter/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/quickstarts/flutter/android/app/src/main/kotlin/dev/thunderid/flutter_quickstart/MainActivity.kt b/quickstarts/flutter/android/app/src/main/kotlin/dev/thunderid/flutter_quickstart/MainActivity.kt new file mode 100644 index 0000000..096090a --- /dev/null +++ b/quickstarts/flutter/android/app/src/main/kotlin/dev/thunderid/flutter_quickstart/MainActivity.kt @@ -0,0 +1,5 @@ +package dev.thunderid.flutter_quickstart + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/quickstarts/flutter/android/app/src/main/res/drawable-v21/launch_background.xml b/quickstarts/flutter/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/quickstarts/flutter/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/quickstarts/flutter/android/app/src/main/res/drawable/launch_background.xml b/quickstarts/flutter/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/quickstarts/flutter/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/quickstarts/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/quickstarts/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/quickstarts/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/quickstarts/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/quickstarts/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/quickstarts/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/quickstarts/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/quickstarts/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/quickstarts/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/quickstarts/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/quickstarts/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/quickstarts/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/quickstarts/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/quickstarts/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/quickstarts/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/quickstarts/flutter/android/app/src/main/res/values-night/styles.xml b/quickstarts/flutter/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/quickstarts/flutter/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/quickstarts/flutter/android/app/src/main/res/values/styles.xml b/quickstarts/flutter/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/quickstarts/flutter/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/quickstarts/flutter/android/app/src/profile/AndroidManifest.xml b/quickstarts/flutter/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/quickstarts/flutter/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/quickstarts/flutter/android/build.gradle.kts b/quickstarts/flutter/android/build.gradle.kts new file mode 100644 index 0000000..d08ad63 --- /dev/null +++ b/quickstarts/flutter/android/build.gradle.kts @@ -0,0 +1,25 @@ +allprojects { + repositories { + google() + mavenCentral() + maven { url = uri("https://jitpack.io") } + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/quickstarts/flutter/android/flutter_quickstart_android.iml b/quickstarts/flutter/android/flutter_quickstart_android.iml new file mode 100644 index 0000000..3bc4b3b --- /dev/null +++ b/quickstarts/flutter/android/flutter_quickstart_android.iml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/quickstarts/flutter/android/gradle.properties b/quickstarts/flutter/android/gradle.properties new file mode 100644 index 0000000..fbee1d8 --- /dev/null +++ b/quickstarts/flutter/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/quickstarts/flutter/android/gradle/wrapper/gradle-wrapper.properties b/quickstarts/flutter/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/quickstarts/flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/quickstarts/flutter/android/settings.gradle.kts b/quickstarts/flutter/android/settings.gradle.kts new file mode 100644 index 0000000..ca7fe06 --- /dev/null +++ b/quickstarts/flutter/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/quickstarts/flutter/assetlinks.json.example b/quickstarts/flutter/assetlinks.json.example new file mode 100644 index 0000000..56d12f3 --- /dev/null +++ b/quickstarts/flutter/assetlinks.json.example @@ -0,0 +1,12 @@ +[ + { + "relation": ["delegate_permission/common.get_login_creds"], + "target": { + "namespace": "android_app", + "package_name": "dev.thunderid.flutter_quickstart", + "sha256_cert_fingerprints": [ + "" + ] + } + } +] diff --git a/quickstarts/flutter/flutter_b2c.iml b/quickstarts/flutter/flutter_b2c.iml new file mode 100644 index 0000000..f66303d --- /dev/null +++ b/quickstarts/flutter/flutter_b2c.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/quickstarts/flutter/flutter_quickstart.iml b/quickstarts/flutter/flutter_quickstart.iml new file mode 100644 index 0000000..f66303d --- /dev/null +++ b/quickstarts/flutter/flutter_quickstart.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/quickstarts/flutter/ios/.gitignore b/quickstarts/flutter/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/quickstarts/flutter/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/quickstarts/flutter/ios/Flutter/AppFrameworkInfo.plist b/quickstarts/flutter/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..391a902 --- /dev/null +++ b/quickstarts/flutter/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/quickstarts/flutter/ios/Flutter/Debug.xcconfig b/quickstarts/flutter/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/quickstarts/flutter/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/quickstarts/flutter/ios/Flutter/Release.xcconfig b/quickstarts/flutter/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/quickstarts/flutter/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/quickstarts/flutter/ios/Podfile b/quickstarts/flutter/ios/Podfile new file mode 100644 index 0000000..d649ddf --- /dev/null +++ b/quickstarts/flutter/ios/Podfile @@ -0,0 +1,44 @@ +platform :ios, '16.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + pod 'ThunderID', :git => 'https://github.com/thunder-id/ios-sdks', :branch => 'main' + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/quickstarts/flutter/ios/Runner.xcodeproj/project.pbxproj b/quickstarts/flutter/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..b6af898 --- /dev/null +++ b/quickstarts/flutter/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,737 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 25604BB0C5D4253EA9F66207 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8E69A3A38C99D0AD4E4C2F2D /* Pods_Runner.framework */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 6A222F8C540B83806C61B446 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C0011CBFF79FEA703C01697 /* Pods_RunnerTests.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0BA1155FF8054CAFB9195FE7 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4C0011CBFF79FEA703C01697 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 604F03390D50C5B0431BCDCE /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 65AAD9653F9261F8FF02E12E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7E492B9724CEC84F2DD64C48 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 8E69A3A38C99D0AD4E4C2F2D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7884E8682EC3CC0500C636F2 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + BB4ADE648BDC5D1EB1C7F3DE /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + C4DFF1B3D699B2E866A4C76D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5D3F5A9CEC455807F803B9A2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6A222F8C540B83806C61B446 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 25604BB0C5D4253EA9F66207 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1D45EFF127630E5FEDBD8758 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8E69A3A38C99D0AD4E4C2F2D /* Pods_Runner.framework */, + 4C0011CBFF79FEA703C01697 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 3C32D4ABDE6503950307A95A /* Pods */ = { + isa = PBXGroup; + children = ( + 7E492B9724CEC84F2DD64C48 /* Pods-Runner.debug.xcconfig */, + 65AAD9653F9261F8FF02E12E /* Pods-Runner.release.xcconfig */, + 0BA1155FF8054CAFB9195FE7 /* Pods-Runner.profile.xcconfig */, + 604F03390D50C5B0431BCDCE /* Pods-RunnerTests.debug.xcconfig */, + C4DFF1B3D699B2E866A4C76D /* Pods-RunnerTests.release.xcconfig */, + BB4ADE648BDC5D1EB1C7F3DE /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 3C32D4ABDE6503950307A95A /* Pods */, + 1D45EFF127630E5FEDBD8758 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + 7884E8682EC3CC0500C636F2 /* Runner.entitlements */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 089A43EE0800B27345DAE923 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 5D3F5A9CEC455807F803B9A2 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + A3D4D7F2FFCA68FBDAEFE190 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + C6E0620E66429D285910F9F8 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 089A43EE0800B27345DAE923 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + A3D4D7F2FFCA68FBDAEFE190 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + C6E0620E66429D285910F9F8 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.thunderid.Quickstart; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 604F03390D50C5B0431BCDCE /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.thunderid.Quickstart.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C4DFF1B3D699B2E866A4C76D /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.thunderid.Quickstart.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = BB4ADE648BDC5D1EB1C7F3DE /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.thunderid.Quickstart.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.thunderid.Quickstart; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.thunderid.Quickstart; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/quickstarts/flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/quickstarts/flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/quickstarts/flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/quickstarts/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/quickstarts/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/quickstarts/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/quickstarts/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/quickstarts/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/quickstarts/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/quickstarts/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/quickstarts/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..3d095f2 --- /dev/null +++ b/quickstarts/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "ios-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/thunder-id/ios-sdks", + "state" : { + "branch" : "main", + "revision" : "1ed224e4697acb0dace4604de6c2eeeb5ff9c353" + } + } + ], + "version" : 2 +} diff --git a/quickstarts/flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/quickstarts/flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/quickstarts/flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/quickstarts/flutter/ios/Runner.xcworkspace/contents.xcworkspacedata b/quickstarts/flutter/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/quickstarts/flutter/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/quickstarts/flutter/ios/Runner/AppDelegate.swift b/quickstarts/flutter/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c30b367 --- /dev/null +++ b/quickstarts/flutter/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..d41fd82 Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..0418749 Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..b8df5ec Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..81bea86 Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..2c7701b Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..92298e2 Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..ff48b7b Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..6cd356c Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..5b95fb9 Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..4cc3cf3 Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..4cc3cf3 Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..9e7be17 Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..0f02bae Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..4fd28ab Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..1ee055d Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/quickstarts/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/quickstarts/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/quickstarts/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/quickstarts/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/quickstarts/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/quickstarts/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/quickstarts/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/quickstarts/flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard b/quickstarts/flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/quickstarts/flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/quickstarts/flutter/ios/Runner/Base.lproj/Main.storyboard b/quickstarts/flutter/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/quickstarts/flutter/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/quickstarts/flutter/ios/Runner/Info.plist b/quickstarts/flutter/ios/Runner/Info.plist new file mode 100644 index 0000000..26fe2bb --- /dev/null +++ b/quickstarts/flutter/ios/Runner/Info.plist @@ -0,0 +1,75 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Quickstart + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + flutter_quickstart + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/quickstarts/flutter/ios/Runner/Runner-Bridging-Header.h b/quickstarts/flutter/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/quickstarts/flutter/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/quickstarts/flutter/ios/Runner/Runner.entitlements b/quickstarts/flutter/ios/Runner/Runner.entitlements new file mode 100644 index 0000000..6ba0b71 --- /dev/null +++ b/quickstarts/flutter/ios/Runner/Runner.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.associated-domains + + webcredentials:your-thunderid-domain.example + + + diff --git a/quickstarts/flutter/ios/Runner/SceneDelegate.swift b/quickstarts/flutter/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/quickstarts/flutter/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/quickstarts/flutter/ios/RunnerTests/RunnerTests.swift b/quickstarts/flutter/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/quickstarts/flutter/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/quickstarts/flutter/lib/app.dart b/quickstarts/flutter/lib/app.dart new file mode 100644 index 0000000..0b5631a --- /dev/null +++ b/quickstarts/flutter/lib/app.dart @@ -0,0 +1,63 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/material.dart'; +import 'package:thunderid_flutter/thunderid_flutter.dart'; +import 'screens/auth_screen.dart'; +import 'screens/home_screen.dart'; + +class QuickstartApp extends StatelessWidget { + const QuickstartApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'ThunderID Quickstart', + theme: ThemeData( + colorSchemeSeed: const Color(0xFF3688FF), + useMaterial3: true, + ), + home: const _RootScreen(), + ); + } +} + +class _RootScreen extends StatelessWidget { + const _RootScreen(); + + @override + Widget build(BuildContext context) { + final thunder = ThunderIDProvider.of(context); + + if (thunder.isLoading) { + return const Scaffold( + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + SizedBox(height: 12), + Text('Starting ThunderID…'), + ], + ), + ), + ); + } + + if (thunder.error != null) { + return Scaffold( + body: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + 'Configuration error: ${thunder.error}\n\nCheck your .env values.', + textAlign: TextAlign.center, + ), + ), + ), + ); + } + + return thunder.isSignedIn ? const HomeScreen() : const AuthScreen(); + } +} diff --git a/quickstarts/flutter/lib/main.dart b/quickstarts/flutter/lib/main.dart new file mode 100644 index 0000000..a852e44 --- /dev/null +++ b/quickstarts/flutter/lib/main.dart @@ -0,0 +1,30 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:thunderid_flutter/thunderid_flutter.dart'; +import 'app.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await dotenv.load(); + runApp( + ThunderIDProvider( + config: ThunderIDConfig( + baseUrl: dotenv.env['THUNDERID_BASE_URL']!, + applicationId: dotenv.env['THUNDERID_APP_ID'], + scopes: const ['openid', 'profile', 'email'], + attestationEnabled: + dotenv.env['THUNDERID_ATTESTATION_ENABLED']?.toLowerCase() == 'true', + cloudProjectNumber: + int.tryParse(dotenv.env['THUNDERID_CLOUD_PROJECT_NUMBER'] ?? ''), + // Local ThunderID instances serve a self-signed certificate, which Android + // rejects. Debug builds only; release builds keep strict TLS. + allowInsecureConnections: kDebugMode, + ), + child: const QuickstartApp(), + ), + ); +} diff --git a/quickstarts/flutter/lib/screens/auth_screen.dart b/quickstarts/flutter/lib/screens/auth_screen.dart new file mode 100644 index 0000000..e3b24b8 --- /dev/null +++ b/quickstarts/flutter/lib/screens/auth_screen.dart @@ -0,0 +1,328 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:thunderid_flutter/thunderid_flutter.dart'; + +class AuthScreen extends StatefulWidget { + const AuthScreen({super.key}); + + @override + State createState() => _AuthScreenState(); +} + +class _AuthScreenState extends State { + static const _blue = Color(0xFF3688FF); + static const _muted = Color(0xFF5A7085); + static const _darkBg = Color(0xFF080f1c); + static const _lightBg = Color(0xFFF7F9FC); + + static const _featureTags = ['OAuth 2.0', 'PKCE', 'JWT', 'MFA', 'SSO']; + + void _showSheet(BuildContext context, String mode) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => _AuthSheet(initialMode: mode), + ); + } + + @override + Widget build(BuildContext context) { + final brightness = Theme.of(context).brightness; + final isDark = brightness == Brightness.dark; + final bgColor = isDark ? _darkBg : _lightBg; + final textColor = isDark ? Colors.white : const Color(0xFF05213F); + + const logoHeight = 72.0; + const logoWidth = logoHeight * (207 / 257); + + return Scaffold( + backgroundColor: bgColor, + body: Stack( + children: [ + SafeArea( + child: Column( + children: [ + // ── Identity section ──────────────────────────────────────── + Expanded( + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 28), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CustomPaint( + painter: _TdMarkPainter(dark: isDark), + size: const Size(logoWidth, logoHeight), + ), + const SizedBox(height: 28), + Text( + 'Authentication for developers.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + letterSpacing: -1.4, + color: textColor, + height: 1.2, + ), + ), + const SizedBox(height: 12), + const Text( + 'OAuth 2.0, PKCE, MFA, and JWT - out of the box in minutes.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + color: _muted, + height: 1.5, + ), + ), + const SizedBox(height: 20), + SizedBox( + height: 36, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: _featureTags.map((tag) { + return Padding( + padding: const EdgeInsets.only(right: 8), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + border: Border.all( + color: _blue.withValues(alpha: 0.4), + ), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + tag, + style: const TextStyle( + fontSize: 12, + color: _blue, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + }).toList(), + ), + ), + ), + ], + ), + ), + ), + ), + + // ── Bottom CTAs ───────────────────────────────────────────── + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 48), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ElevatedButton.icon( + onPressed: () => _showSheet(context, 'signup'), + icon: const Icon(Icons.arrow_forward, size: 18), + label: const Text('Get started'), + style: ElevatedButton.styleFrom( + backgroundColor: _blue, + foregroundColor: Colors.white, + minimumSize: const Size.fromHeight(52), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(height: 12), + OutlinedButton( + onPressed: () => _showSheet(context, 'login'), + style: OutlinedButton.styleFrom( + foregroundColor: _blue, + side: const BorderSide(color: _blue), + minimumSize: const Size.fromHeight(52), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + child: const Text('Sign in'), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Auth bottom sheet +// ───────────────────────────────────────────────────────────────────────────── + +class _AuthSheet extends StatefulWidget { + final String initialMode; + const _AuthSheet({required this.initialMode}); + + @override + State<_AuthSheet> createState() => _AuthSheetState(); +} + +class _AuthSheetState extends State<_AuthSheet> { + late String _mode; + + @override + void initState() { + super.initState(); + _mode = widget.initialMode; + } + + @override + Widget build(BuildContext context) { + final applicationId = dotenv.env['THUNDERID_APP_ID'] ?? ''; + final brightness = Theme.of(context).brightness; + final isDark = brightness == Brightness.dark; + final sheetBg = isDark ? const Color(0xFF0d1627) : Colors.white; + + return DraggableScrollableSheet( + initialChildSize: 0.85, + minChildSize: 0.5, + maxChildSize: 0.95, + expand: false, + builder: (ctx, scrollController) { + return Container( + decoration: BoxDecoration( + color: sheetBg, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + const SizedBox(height: 12), + Container( + width: 36, + height: 4, + decoration: BoxDecoration( + color: Colors.grey.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 8), + Expanded( + child: SingleChildScrollView( + controller: scrollController, + padding: const EdgeInsets.fromLTRB(24, 8, 24, 32), + child: _buildSheetContent(ctx, applicationId), + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildSheetContent(BuildContext context, String applicationId) { + // The bottom sheet is pushed via the Navigator, so it doesn't automatically dismiss + // just because the app's root widget swaps from AuthScreen to HomeScreen once signed in β€” + // it must be popped explicitly once the flow completes, or it's left stuck showing its + // final "complete" spinner on top of the (already rendered) home screen underneath. + switch (_mode) { + case 'login': + return SignIn( + applicationId: applicationId, + onSuccess: (_) => Navigator.of(context).pop(), + ); + case 'signup': + return SignUp( + applicationId: applicationId, + onSuccess: () => Navigator.of(context).pop(), + ); + default: + return const SizedBox.shrink(); + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// ThunderID logo mark painter +// ───────────────────────────────────────────────────────────────────────────── + +class _TdMarkPainter extends CustomPainter { + final bool dark; + const _TdMarkPainter({required this.dark}); + + @override + void paint(Canvas canvas, Size size) { + final scale = size.height / 257; + + canvas.save(); + canvas.scale(scale, scale); + + // Path 1 β€” dark fill (onSurface color) + final darkPaint = Paint() + ..color = dark ? Colors.white : const Color(0xFF05213F) + ..style = PaintingStyle.fill; + + final path1 = Path() + ..moveTo(55.4763, 26.4391) + ..lineTo(58.8866, 0) + ..lineTo(0, 0) + ..lineTo(0, 26.4391) + ..close(); + + canvas.drawPath(path1, darkPaint); + + // Path 2 & 3 β€” blue fill + final bluePaint = Paint() + ..color = const Color(0xFF3688FF) + ..style = PaintingStyle.fill; + + final path2 = Path() + ..moveTo(39.8438, 147.407) + ..lineTo(49.5455, 72.2839) + ..lineTo(0, 72.2839) + ..lineTo(0, 256.743) + ..lineTo(60.5602, 256.743) + ..lineTo(80.048, 147.407) + ..lineTo(39.8438, 147.407) + ..close(); + + canvas.drawPath(path2, bluePaint); + + final path3 = Path() + ..moveTo(192.42, 59.361) + ..cubicTo(182.782, 40.2307, 168.929, 25.5705, 150.903, 15.3381) + ..cubicTo(145.501, 12.2662, 139.761, 9.6605, 133.703, 7.5208) + ..lineTo(115.401, 103.702) + ..lineTo(159.757, 103.702) + ..lineTo(76.2987, 256.743) + ..lineTo(83.3735, 256.743) + ..cubicTo(109.449, 256.743, 131.69, 251.574, 150.14, 241.236) + ..cubicTo(168.569, 230.897, 182.634, 216.131, 192.356, 196.959) + ..cubicTo(202.058, 177.765, 206.909, 154.8, 206.909, 128.043) + ..cubicTo(206.909, 101.286, 202.079, 78.5123, 192.441, 59.3821) + ..lineTo(192.42, 59.361) + ..close(); + + canvas.drawPath(path3, bluePaint); + + canvas.restore(); + } + + @override + bool shouldRepaint(_TdMarkPainter oldDelegate) => oldDelegate.dark != dark; +} diff --git a/quickstarts/flutter/lib/screens/home_screen.dart b/quickstarts/flutter/lib/screens/home_screen.dart new file mode 100644 index 0000000..ec7f8d7 --- /dev/null +++ b/quickstarts/flutter/lib/screens/home_screen.dart @@ -0,0 +1,1066 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:thunderid_flutter/thunderid_flutter.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + String _screen = 'home'; + + @override + Widget build(BuildContext context) { + switch (_screen) { + case 'profile': + return _ProfileScreen(onBack: () => setState(() => _screen = 'home')); + case 'token': + return _TokenScreen(onBack: () => setState(() => _screen = 'home')); + default: + return _HomeTabScreen( + onNavigate: (screen) => setState(() => _screen = screen), + ); + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Design constants +// ───────────────────────────────────────────────────────────────────────────── + +const _kBlue = Color(0xFF3688FF); +const _kGreen = Color(0xFF2FBD6B); +const _kRed = Color(0xFFD95757); +const _kMuted = Color(0xFF5A7085); +const _kTextDark = Color(0xFF05213F); +const _kCodeBg = Color(0xFF0b1120); + +// ───────────────────────────────────────────────────────────────────────────── +// Home tab +// ───────────────────────────────────────────────────────────────────────────── + +/// Converts a raw claim value (typically a JSON number bridged from the +/// platform channel) into whole seconds-since-epoch, or `null` if the claim +/// is absent or not numeric. +int? _epochSecondsFromClaim(dynamic value) { + if (value is num) return value.toInt(); + return null; +} + +/// Formats a unix-seconds timestamp as a local `h:mm AM/PM` time string. +String _formatLocalTime(int? epochSeconds) { + if (epochSeconds == null) return 'β€”'; + final dt = DateTime.fromMillisecondsSinceEpoch(epochSeconds * 1000); + var hour12 = dt.hour % 12; + if (hour12 == 0) hour12 = 12; + final minute = dt.minute.toString().padLeft(2, '0'); + final period = dt.hour >= 12 ? 'PM' : 'AM'; + return '$hour12:$minute $period'; +} + +/// Formats the number of whole seconds remaining until session expiry. +/// Returns `β€”` once the claim is unavailable or the session has expired. +String _formatCountdown(int? secondsLeft) { + if (secondsLeft == null || secondsLeft <= 0) return 'β€”'; + if (secondsLeft < 3600) { + final minutes = secondsLeft ~/ 60; + final seconds = secondsLeft % 60; + return '${minutes}m ${seconds}s'; + } + final hours = secondsLeft ~/ 3600; + final minutes = (secondsLeft % 3600) ~/ 60; + return '${hours}h ${minutes}m'; +} + +/// Returns a time-of-day-aware greeting, e.g. "Good afternoon, Alex.". +String _greeting(String name) { + final hour = DateTime.now().hour; + final String timeOfDay; + if (hour < 12) { + timeOfDay = 'morning'; + } else if (hour < 17) { + timeOfDay = 'afternoon'; + } else { + timeOfDay = 'evening'; + } + return 'Good $timeOfDay, $name.'; +} + +const List _kWeekdayNames = [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', +]; + +const List _kMonthNames = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', +]; + +/// Formats today's date as e.g. "FRIDAY, JULY 31". +String _dateLabel() { + final now = DateTime.now(); + final weekday = _kWeekdayNames[now.weekday - 1]; + final month = _kMonthNames[now.month - 1]; + return '$weekday, $month ${now.day}'.toUpperCase(); +} + +class _HomeTabScreen extends StatefulWidget { + final void Function(String) onNavigate; + const _HomeTabScreen({required this.onNavigate}); + + @override + State<_HomeTabScreen> createState() => _HomeTabScreenState(); +} + +class _HomeTabScreenState extends State<_HomeTabScreen> { + Timer? _ticker; + int _nowSeconds = DateTime.now().millisecondsSinceEpoch ~/ 1000; + + @override + void initState() { + super.initState(); + _ticker = Timer.periodic(const Duration(seconds: 1), (_) { + if (mounted) { + setState(() { + _nowSeconds = DateTime.now().millisecondsSinceEpoch ~/ 1000; + }); + } + }); + } + + @override + void dispose() { + _ticker?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final thunder = ThunderIDProvider.of(context); + final user = thunder.user; + final givenName = user?['given_name'] as String?; + final emailPrefix = user?.email?.split('@').first; + final greetingName = (givenName != null && givenName.isNotEmpty) + ? givenName + : (emailPrefix != null && emailPrefix.isNotEmpty) + ? emailPrefix + : 'there'; + final authTime = _epochSecondsFromClaim(user?['auth_time']); + final exp = _epochSecondsFromClaim(user?['exp']); + final secondsLeft = exp != null ? exp - _nowSeconds : null; + final organisation = thunder.widget.config.organizationHandle ?? 'Default'; + + return Scaffold( + body: SingleChildScrollView( + padding: + const EdgeInsets.only(top: 72, left: 20, right: 20, bottom: 48), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Hero: avatar + (date/session row, greeting row) ───────────── + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const UserAvatar(size: 52), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + _dateLabel(), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: _kMuted, + letterSpacing: 1.0, + ), + ), + const SizedBox(width: 6), + Container( + width: 3, + height: 3, + decoration: BoxDecoration( + color: _kMuted.withValues(alpha: 0.4), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 6), + Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + color: _kGreen, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 5), + const Text( + 'Session active', + style: TextStyle( + fontSize: 12, + color: _kGreen, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + _greeting(greetingName), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: _kTextDark, + ), + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 20), + const Divider(), + const SizedBox(height: 16), + + // ── Stats row ────────────────────────────────────────────────── + IntrinsicHeight( + child: Row( + children: [ + _StatCell( + label: 'Signed in at', value: _formatLocalTime(authTime)), + const VerticalDivider(width: 32), + _StatCell( + label: 'Session expires in', + value: _formatCountdown(secondsLeft)), + const VerticalDivider(width: 32), + _StatCell(label: 'Organisation', value: organisation), + ], + ), + ), + + const SizedBox(height: 28), + + // ── What's next ──────────────────────────────────────────────── + Text( + 'WHAT\'S NEXT', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + letterSpacing: 1.0, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 4), + + const _StepRow( + number: '01', + title: 'Explore use cases', + subtitle: + 'See what you can build β€” auth flows for web, mobile, APIs, and agents.', + href: 'https://thunderid.dev/docs/next/use-cases/overview/', + ), + const _StepRow( + number: '02', + title: 'Learn about flows', + subtitle: + 'Understand how authorization code, PKCE, client credentials, and ' + 'device flows work.', + href: + 'https://thunderid.dev/docs/next/guides/flows/what-are-flows/', + ), + const _StepRow( + number: '03', + title: 'Style your experience', + subtitle: + 'Customize the login UI, branding, and email templates to match ' + 'your product.', + href: 'https://thunderid.dev/docs/next/guides/design/overview/', + ), + const _StepRow( + number: '04', + title: 'Explore SDK APIs', + subtitle: + 'Full Flutter SDK reference β€” widgets, hooks, and configuration options.', + href: 'https://thunderid.dev/docs/next/sdks/flutter/overview/', + ), + + const SizedBox(height: 8), + + // ── Action list ──────────────────────────────────────────────── + const Divider(), + _ActionRow( + icon: Icons.person_outline, + label: 'Manage Account', + onTap: () => widget.onNavigate('profile'), + ), + const Divider(), + _ActionRow( + icon: Icons.key_outlined, + label: 'Token debug', + onTap: () => widget.onNavigate('token'), + ), + const Divider(), + + // ── Sign out ─────────────────────────────────────────────────── + BaseSignOutButton( + builder: (ctx, isLoading) => InkWell( + onTap: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 14), + child: Row( + children: [ + isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: _kRed, + ), + ) + : const Icon(Icons.logout, color: _kRed, size: 20), + const SizedBox(width: 14), + const Text( + 'Sign out', + style: TextStyle( + fontSize: 15, + color: _kRed, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ), + const Divider(), + ], + ), + ), + ); + } +} + +class _StatCell extends StatelessWidget { + final String label; + final String value; + const _StatCell({required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: _kTextDark, + ), + ), + const SizedBox(height: 2), + Text( + label, + style: const TextStyle(fontSize: 12, color: _kMuted), + ), + ], + ), + ); + } +} + +class _StepRow extends StatelessWidget { + final String number; + final String title; + final String subtitle; + final String href; + const _StepRow({ + required this.number, + required this.title, + required this.subtitle, + required this.href, + }); + + Future _open() async { + final uri = Uri.tryParse(href); + if (uri == null) return; + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const Divider(), + InkWell( + onTap: _open, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + Text( + number, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: _kBlue, + fontFamily: 'monospace', + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: _kTextDark, + ), + ), + const SizedBox(height: 2), + Text( + subtitle, + style: const TextStyle(fontSize: 13, color: _kMuted), + ), + ], + ), + ), + const Icon(Icons.chevron_right, color: _kMuted, size: 20), + ], + ), + ), + ), + ], + ); + } +} + +class _ActionRow extends StatelessWidget { + final IconData icon; + final String label; + final VoidCallback onTap; + const _ActionRow({ + required this.icon, + required this.label, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 14), + child: Row( + children: [ + Icon(icon, size: 20, color: _kMuted), + const SizedBox(width: 14), + Expanded( + child: Text( + label, + style: const TextStyle( + fontSize: 15, + color: _kTextDark, + fontWeight: FontWeight.w500, + ), + ), + ), + const Icon(Icons.chevron_right, color: _kMuted, size: 20), + ], + ), + ), + ); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Profile screen +// ───────────────────────────────────────────────────────────────────────────── + +/// Manage Account hub: a back link, then one navigation row per section. Each row pushes a +/// section screen holding the actual SDK component - the styled `UserProfile` and +/// `ChangeCredential` widgets carry their own presentation (see `internal/account_style.dart` +/// in the SDK), so neither this screen nor `_AccountSectionScreen` hand-roll any of it. +class _ProfileScreen extends StatelessWidget { + final VoidCallback onBack; + const _ProfileScreen({required this.onBack}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + _BackChevron(label: 'Home', onTap: onBack), + const SizedBox(height: 8), + const Text( + 'Manage Account', + style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold, color: _kTextDark), + ), + const SizedBox(height: 20), + _AccountSettingsRow( + icon: Icons.person_outline, + label: 'Personal info', + onTap: () => _openSection( + context, + 'Personal info', + UserProfile( + onSaved: () => ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Profile saved')), + ), + ), + ), + ), + _AccountSettingsRow( + icon: Icons.shield_outlined, + label: 'Security', + onTap: () => _openSection(context, 'Security', const ChangeCredential()), + ), + ], + ), + ), + ), + ); + } + + void _openSection(BuildContext context, String title, Widget child) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _AccountSectionScreen(title: title, child: child), + ), + ); + } +} + +class _AccountSettingsRow extends StatelessWidget { + final IconData icon; + final String label; + final VoidCallback onTap; + + const _AccountSettingsRow({required this.icon, required this.label, required this.onTap}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + children: [ + Icon(icon, color: _kTextDark, size: 22), + const SizedBox(width: 16), + Expanded(child: Text(label, style: const TextStyle(fontSize: 16, color: _kTextDark))), + const Icon(Icons.chevron_right, color: _kMuted, size: 20), + ], + ), + ), + ); + } +} + +class _AccountSectionScreen extends StatelessWidget { + final String title; + final Widget child; + + const _AccountSectionScreen({required this.title, required this.child}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + _BackChevron(label: 'Manage Account', onTap: () => Navigator.of(context).pop()), + const SizedBox(height: 8), + Text( + title, + style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: _kTextDark), + ), + const SizedBox(height: 20), + child, + ], + ), + ), + ), + ); + } +} + +/// Back control used by every screen nested under the home tab: a chevron followed by the name +/// of the screen it returns to, matching the Android/iOS SDKs' "β€Ή Home" convention. +/// +/// It carries a fixed Semantics id rather than relying on [label] text for E2E targeting, since +/// that text differs per screen. One shared id is unambiguous here: exactly one of these is ever +/// on screen at a time, since each is a different screen's own back control in a single +/// push/pop chain, never siblings in the same view. +class _BackChevron extends StatelessWidget { + final String label; + final VoidCallback onTap; + const _BackChevron({required this.label, required this.onTap}); + + @override + Widget build(BuildContext context) { + return Semantics( + identifier: 'thunderid-nav-back', + // The chevron glyph carries its own built-in padding, so the tap target's box left edge + // doesn't line up with the text below it. Shifting the whole control left compensates, + // rather than a negative Padding, which Flutter rejects. + child: Transform.translate( + offset: const Offset(-6, 0), + child: InkWell( + key: const Key('thunderid-nav-back'), + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.only(top: 6, bottom: 6, right: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.chevron_left, color: _kBlue, size: 26), + const SizedBox(width: 2), + Text( + label, + style: const TextStyle( + color: _kBlue, + fontSize: 15, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Token debug screen +// ───────────────────────────────────────────────────────────────────────────── + +class _TokenScreen extends StatefulWidget { + final VoidCallback onBack; + const _TokenScreen({required this.onBack}); + + @override + State<_TokenScreen> createState() => _TokenScreenState(); +} + +class _TokenScreenState extends State<_TokenScreen> { + String? _token; + String _payload = '{}'; + bool _loading = true; + String? _error; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_loading) { + _loadToken(); + } + } + + Future _loadToken() async { + final thunder = ThunderIDProvider.of(context); + try { + final tok = await thunder.client.getAccessToken(); + if (!mounted) return; + setState(() { + _token = tok; + _payload = _decodePayload(tok); + _loading = false; + }); + } catch (e) { + if (!mounted) return; + setState(() { + _error = e.toString(); + _loading = false; + }); + } + } + + String _decodePayload(String token) { + try { + final parts = token.split('.'); + if (parts.length != 3) return '{}'; + final padded = parts[1].padRight((parts[1].length + 3) ~/ 4 * 4, '='); + return utf8.decode(base64Url.decode(padded)); + } catch (_) { + return '{}'; + } + } + + Widget _buildExpiryBadge() { + try { + final map = jsonDecode(_payload) as Map; + final exp = map['exp']; + if (exp == null) return const SizedBox.shrink(); + final expSecs = (exp as num).toInt(); + final nowSecs = DateTime.now().millisecondsSinceEpoch ~/ 1000; + final diffSecs = expSecs - nowSecs; + if (diffSecs <= 0) { + return const _Badge(label: 'Expired', color: _kRed); + } + final mins = (diffSecs / 60).ceil(); + return _Badge(label: '$mins min', color: _kGreen); + } catch (_) { + return const SizedBox.shrink(); + } + } + + String _issuer() { + try { + final map = jsonDecode(_payload) as Map; + return (map['iss'] as String?) ?? 'β€”'; + } catch (_) { + return 'β€”'; + } + } + + String _scopes() { + try { + final map = jsonDecode(_payload) as Map; + final scp = map['scp'] ?? map['scope']; + if (scp == null) return 'β€”'; + if (scp is List) return scp.join(', '); + return scp.toString(); + } catch (_) { + return 'β€”'; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Back button ────────────────────────────────────────────── + const SizedBox(height: 16), + _BackChevron(label: 'Home', onTap: widget.onBack), + + const SizedBox(height: 8), + + // ── Title row ──────────────────────────────────────────────── + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Token debug', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: _kTextDark, + ), + ), + SizedBox(height: 2), + Text( + 'Access token and claims', + style: TextStyle(fontSize: 13, color: _kMuted), + ), + ], + ), + ), + if (!_loading && _token != null) _buildExpiryBadge(), + ], + ), + + const SizedBox(height: 20), + + if (_loading) + const Center( + child: Padding( + padding: EdgeInsets.all(40), + child: CircularProgressIndicator(color: _kBlue), + ), + ) + else if (_error != null) + Text( + 'Error: $_error', + style: const TextStyle(color: _kRed, fontSize: 13), + ) + else ...[ + // ── JWT token display ──────────────────────────────────── + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _kCodeBg, + borderRadius: BorderRadius.circular(10), + ), + child: _token != null + ? _buildJwtRichText(_token!) + : const SizedBox.shrink(), + ), + + const SizedBox(height: 8), + + // ── Copy button ────────────────────────────────────────── + Align( + alignment: Alignment.centerRight, + child: TextButton.icon( + onPressed: () async { + if (_token != null) { + await Clipboard.setData(ClipboardData(text: _token!)); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Token copied to clipboard'), + duration: Duration(seconds: 2), + ), + ); + } + } + }, + icon: const Icon(Icons.copy, size: 14, color: _kMuted), + label: const Text( + 'Copy', + style: TextStyle(fontSize: 13, color: _kMuted), + ), + ), + ), + + const SizedBox(height: 16), + + // ── JWT Payload section ────────────────────────────────── + const Text( + 'JWT PAYLOAD', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + letterSpacing: 1.0, + color: _kMuted, + ), + ), + const SizedBox(height: 8), + Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: _kCodeBg, + borderRadius: BorderRadius.circular(10), + ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Text( + _prettyJson(_payload), + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + color: Color(0xFF79c0ff), + height: 1.5, + ), + ), + ), + ), + + const SizedBox(height: 20), + + // ── Metadata rows ──────────────────────────────────────── + const Text( + 'METADATA', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + letterSpacing: 1.0, + color: _kMuted, + ), + ), + const SizedBox(height: 8), + _MetaRow(label: 'Issuer', value: _issuer()), + const Divider(), + _MetaRow(label: 'Scopes', value: _scopes()), + const Divider(), + ], + + const SizedBox(height: 40), + ], + ), + ), + ), + ); + } + + Widget _buildJwtRichText(String token) { + final parts = token.split('.'); + if (parts.length != 3) { + return Text( + token, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 11, + color: Colors.white70, + ), + ); + } + return RichText( + text: TextSpan( + children: [ + TextSpan( + text: parts[0], + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 11, + color: Color(0xFFff7b72), + ), + ), + const TextSpan( + text: '.', + style: TextStyle( + fontFamily: 'monospace', + fontSize: 11, + color: Color(0xFF8b949e), + ), + ), + TextSpan( + text: parts[1], + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 11, + color: Color(0xFF79c0ff), + ), + ), + const TextSpan( + text: '.', + style: TextStyle( + fontFamily: 'monospace', + fontSize: 11, + color: Color(0xFF8b949e), + ), + ), + TextSpan( + text: parts[2], + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 11, + color: Color(0xFF3fb950), + ), + ), + ], + ), + ); + } + + String _prettyJson(String raw) { + try { + final obj = jsonDecode(raw); + const encoder = JsonEncoder.withIndent(' '); + return encoder.convert(obj); + } catch (_) { + return raw; + } + } +} + +class _Badge extends StatelessWidget { + final String label; + final Color color; + const _Badge({required this.label, required this.color}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withValues(alpha: 0.3)), + ), + child: Text( + label, + style: TextStyle( + fontSize: 12, + color: color, + fontWeight: FontWeight.w600, + ), + ), + ); + } +} + +class _MetaRow extends StatelessWidget { + final String label; + final String value; + const _MetaRow({required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 80, + child: Text( + label, + style: const TextStyle( + fontSize: 13, + color: _kMuted, + fontWeight: FontWeight.w500, + ), + ), + ), + Expanded( + child: Text( + value, + style: const TextStyle( + fontSize: 13, + color: _kTextDark, + ), + ), + ), + ], + ), + ); + } +} diff --git a/quickstarts/flutter/lib/screens/profile_screen.dart b/quickstarts/flutter/lib/screens/profile_screen.dart new file mode 100644 index 0000000..bb57109 --- /dev/null +++ b/quickstarts/flutter/lib/screens/profile_screen.dart @@ -0,0 +1,29 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/material.dart'; +import 'package:thunderid_flutter/thunderid_flutter.dart'; + +class ProfileScreen extends StatelessWidget { + const ProfileScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Edit Profile'), + centerTitle: false, + ), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: UserProfile( + onSaved: () => ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Profile saved')), + ), + ), + ), + ), + ); + } +} diff --git a/quickstarts/flutter/pubspec.yaml b/quickstarts/flutter/pubspec.yaml new file mode 100644 index 0000000..bc397d8 --- /dev/null +++ b/quickstarts/flutter/pubspec.yaml @@ -0,0 +1,25 @@ +name: flutter_quickstart +description: ThunderID Flutter B2C sample β€” demonstrates sign-in, profile display, and sign-out. +version: 0.0.0+1 +publish_to: none + +environment: + sdk: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" + +dependencies: + flutter: + sdk: flutter + thunderid_flutter: ^1.2.0 + flutter_dotenv: ^5.1.0 + url_launcher: ^6.2.5 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + +flutter: + uses-material-design: true + assets: + - .env diff --git a/quickstarts/flutter/test/widget_test.dart b/quickstarts/flutter/test/widget_test.dart new file mode 100644 index 0000000..f2829ed --- /dev/null +++ b/quickstarts/flutter/test/widget_test.dart @@ -0,0 +1,48 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:thunderid_flutter/thunderid_flutter.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('io.thunder/sdk'), + (call) async { + switch (call.method) { + case 'initialize': + return true; + case 'isSignedIn': + return false; + default: + return null; + } + }, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(const MethodChannel('io.thunder/sdk'), null); + }); + + testWidgets('shows loading indicator while initializing', (tester) async { + await tester.pumpWidget( + const ThunderIDProvider( + config: ThunderIDConfig( + baseUrl: 'https://localhost:8090', + clientId: 'test', + ), + child: MaterialApp(home: Scaffold(body: Text('App'))), + ), + ); + // First frame: should show the child (ThunderIDProvider manages loading internally) + await tester.pump(); + expect(find.byType(MaterialApp), findsOneWidget); + }); +} diff --git a/quickstarts/flutter/thunderid-config/thunderid-config.yaml b/quickstarts/flutter/thunderid-config/thunderid-config.yaml new file mode 100644 index 0000000..950c5fe --- /dev/null +++ b/quickstarts/flutter/thunderid-config/thunderid-config.yaml @@ -0,0 +1,35 @@ +--- +resource_type: application +# Application used exclusively by the Flutter Quickstart E2E suite. It has no OAuth profile: the +# mobile SDKs authenticate app-natively via the Flow Execution API, so the sample app needs only +# the base URL and this application ID. +# +# The ID is fixed so the sample's Config.plist and the Maestro flows can be committed against a +# known value instead of discovering it at run time. +id: 019e5b10-1001-7a2b-9c3d-4e5f60718293 +ouHandle: default +name: Mobile Quickstart E2E App +description: Application for the ThunderID mobile SDK quickstart E2E suites +type: mobile +# A mobile application must normally prove its binary identity via platform attestation before it +# may initiate a flow directly, otherwise /flow/execute fails with FES-1016. Apple App Attest is +# unavailable in the iOS Simulator and Play Integrity requires real Play Services, so neither can +# be satisfied on the emulated devices this suite drives. devMode skips that check. +# +# Test-only. Never enable this on a real tenant: it removes the guarantee that the client +# initiating a flow is the genuine, unmodified app. +# +# NOTE: POST /import currently drops this block silently (it reports success, then stores +# attestation: null), so the Flutter SDK E2E suite (tests/e2e/run-e2e.sh in +# thunder-id/flutter-sdks) re-applies it over PUT /applications/{id}. Once the import path +# preserves it, that extra step can go. +attestation: + devMode: true +authFlowHandle: default-flow +# The suite registers a fresh user through the UI rather than relying on a seeded account, so +# self-service registration has to be enabled for this application. +registrationFlowHandle: default-flow +isRegistrationFlowEnabled: true +signOutFlowHandle: default-flow +allowedUserTypes: + - Person diff --git a/quickstarts/ios/.gitignore b/quickstarts/ios/.gitignore new file mode 100644 index 0000000..650dc8a --- /dev/null +++ b/quickstarts/ios/.gitignore @@ -0,0 +1,68 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# OS generated files +.DS_Store + +# Local config (use Config.plist.example as template) +Sources/Config.plist diff --git a/quickstarts/ios/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/quickstarts/ios/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/quickstarts/ios/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/quickstarts/ios/.swiftpm/xcode/xcshareddata/xcschemes/Quickstart.xcscheme b/quickstarts/ios/.swiftpm/xcode/xcshareddata/xcschemes/Quickstart.xcscheme new file mode 100644 index 0000000..3a6ec35 --- /dev/null +++ b/quickstarts/ios/.swiftpm/xcode/xcshareddata/xcschemes/Quickstart.xcscheme @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/quickstarts/ios/Info.plist b/quickstarts/ios/Info.plist new file mode 100644 index 0000000..a82c0e5 --- /dev/null +++ b/quickstarts/ios/Info.plist @@ -0,0 +1,58 @@ + + + + + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleName + Quickstart + CFBundleDisplayName + Quickstart + CFBundlePackageType + APPL + LSRequiresIPhoneOS + + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UIApplicationSupportsIndirectInputEvents + + UILaunchScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CFBundleURLTypes + + + CFBundleURLSchemes + + dev.thunderid.Quickstart + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/quickstarts/ios/Package.resolved b/quickstarts/ios/Package.resolved new file mode 100644 index 0000000..7ab754a --- /dev/null +++ b/quickstarts/ios/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "ios-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/thunder-id/ios-sdks", + "state" : { + "revision" : "9145551e7e712d24cd7d6a44d74f2f4c79742788", + "version" : "1.4.0" + } + } + ], + "version" : 2 +} diff --git a/quickstarts/ios/Package.swift b/quickstarts/ios/Package.swift new file mode 100644 index 0000000..5b39d25 --- /dev/null +++ b/quickstarts/ios/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version: 5.9 +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 +import PackageDescription + +let appTarget: Target = .executableTarget( + name: "Quickstart", + dependencies: [ + .product(name: "ThunderIDSwiftUI", package: "ios-sdks") + ], + path: "Sources", + resources: [.process("Config.plist")] +) + +let package = Package( + name: "Quickstart", + platforms: [.iOS(.v16), .macOS(.v13)], + dependencies: [ + .package(url: "https://github.com/thunder-id/ios-sdks", from: "1.4.0") + ], + targets: [appTarget] +) diff --git a/quickstarts/ios/Quickstart.xcodeproj/project.pbxproj b/quickstarts/ios/Quickstart.xcodeproj/project.pbxproj new file mode 100644 index 0000000..c2ad1d2 --- /dev/null +++ b/quickstarts/ios/Quickstart.xcodeproj/project.pbxproj @@ -0,0 +1,352 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + DEADBEEF000000000011DEAD /* ThunderIDSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = DEADBEEF000000000010DEAD /* ThunderIDSwiftUI */; }; + DEADBEEF000000000012DEAD /* Sources/Config.plist in Resources */ = {isa = PBXBuildFile; fileRef = DEADBEEF000000000013DEAD /* Sources/Config.plist */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + DEADBEEF000000000002DEAD /* Quickstart.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Quickstart.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DEADBEEF000000000013DEAD /* Sources/Config.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Sources/Config.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + DEADBEEF000000000005DEAD /* Sources */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Sources; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + DEADBEEF000000000007DEAD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DEADBEEF000000000011DEAD /* ThunderIDSwiftUI in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + DEADBEEF000000000003DEAD = { + isa = PBXGroup; + children = ( + DEADBEEF000000000005DEAD /* Sources */, + DEADBEEF000000000013DEAD /* Sources/Config.plist */, + DEADBEEF000000000004DEAD /* Products */, + ); + sourceTree = ""; + }; + DEADBEEF000000000004DEAD /* Products */ = { + isa = PBXGroup; + children = ( + DEADBEEF000000000002DEAD /* Quickstart.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + DEADBEEF000000000001DEAD /* Quickstart */ = { + isa = PBXNativeTarget; + buildConfigurationList = DEADBEEF00000000000ADEAD /* Build configuration list for PBXNativeTarget "Quickstart" */; + buildPhases = ( + DEADBEEF000000000006DEAD /* Sources */, + DEADBEEF000000000007DEAD /* Frameworks */, + DEADBEEF000000000008DEAD /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + DEADBEEF000000000005DEAD /* Sources */, + ); + name = Quickstart; + packageProductDependencies = ( + DEADBEEF000000000010DEAD /* ThunderIDSwiftUI */, + ); + productName = Quickstart; + productReference = DEADBEEF000000000002DEAD /* Quickstart.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + DEADBEEF000000000000DEAD /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2600; + LastUpgradeCheck = 2620; + TargetAttributes = { + DEADBEEF000000000001DEAD = { + CreatedOnToolsVersion = 26.0; + }; + }; + }; + buildConfigurationList = DEADBEEF000000000009DEAD /* Build configuration list for PBXProject "Quickstart" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = DEADBEEF000000000003DEAD; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + DEADBEEF00000000000FDEAD /* XCRemoteSwiftPackageReference "ios-sdks" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = DEADBEEF000000000004DEAD /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + DEADBEEF000000000001DEAD /* Quickstart */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + DEADBEEF000000000008DEAD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DEADBEEF000000000012DEAD /* Sources/Config.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + DEADBEEF000000000006DEAD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + DEADBEEF00000000000BDEAD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + DEADBEEF00000000000CDEAD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + DEADBEEF00000000000DDEAD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Sources/Quickstart.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 0.0.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.thunderid.Quickstart; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + DEADBEEF00000000000EDEAD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Sources/Quickstart.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 0.0.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.thunderid.Quickstart; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + DEADBEEF000000000009DEAD /* Build configuration list for PBXProject "Quickstart" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DEADBEEF00000000000BDEAD /* Debug */, + DEADBEEF00000000000CDEAD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DEADBEEF00000000000ADEAD /* Build configuration list for PBXNativeTarget "Quickstart" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DEADBEEF00000000000DDEAD /* Debug */, + DEADBEEF00000000000EDEAD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + DEADBEEF00000000000FDEAD /* XCRemoteSwiftPackageReference "ios-sdks" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/thunder-id/ios-sdks"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.4.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + DEADBEEF000000000010DEAD /* ThunderIDSwiftUI */ = { + isa = XCSwiftPackageProductDependency; + package = DEADBEEF00000000000FDEAD /* XCRemoteSwiftPackageReference "ios-sdks" */; + productName = ThunderIDSwiftUI; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = DEADBEEF000000000000DEAD /* Project object */; +} diff --git a/quickstarts/ios/Quickstart.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/quickstarts/ios/Quickstart.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/quickstarts/ios/Quickstart.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/quickstarts/ios/Quickstart.xcodeproj/xcshareddata/xcschemes/Quickstart.xcscheme b/quickstarts/ios/Quickstart.xcodeproj/xcshareddata/xcschemes/Quickstart.xcscheme new file mode 100644 index 0000000..8e5efbc --- /dev/null +++ b/quickstarts/ios/Quickstart.xcodeproj/xcshareddata/xcschemes/Quickstart.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/quickstarts/ios/README.md b/quickstarts/ios/README.md new file mode 100644 index 0000000..ef4e94d --- /dev/null +++ b/quickstarts/ios/README.md @@ -0,0 +1,41 @@ +# ThunderID iOS Quickstart + +ThunderID iOS Quickstart demonstrates the full authentication lifecycle using the `ThunderID iOS` SDK. + +**Flow demonstrated:** +1. App opens β†’ unauthenticated state (sign-in screen) +2. User initiates sign-in / sign-up β†’ SDK starts app-native Flow Execution +3. User completes the flow +4. Sign-in β†’ authenticated state with profile information, token debugging, and sign-out button. + Sign-up creates the account but does not start a session, so it returns to the landing screen + and the new credentials have to be used to sign in. +5. User taps Sign Out β†’ session terminated, returns to sign-in screen + +The app depends on the published [ThunderID iOS SDK](https://github.com/thunder-id/ios-sdks) +release through Swift Package Manager, so it builds on its own without a checkout of the SDK. + +## Get the Sample + +```bash +git clone https://github.com/thunder-id/samples.git +cd samples/quickstarts/ios +cp Sources/Config.plist.example Sources/Config.plist +``` + +Set `THUNDERID_BASE_URL` and `THUNDERID_APPLICATION_ID` in `Sources/Config.plist`, then open +`Quickstart.xcodeproj` in Xcode and run the `Quickstart` scheme on a simulator or device. +`Sources/Config.plist` is listed in `.gitignore`, so your local values are not committed. + +## Setup & Run + +See [Try the iOS Sample App](https://thunderid.dev/docs/v1.0.x/sdks/ios/guides/try-the-sample-app) +in the iOS SDK docs for prerequisites, configuration, attestation, passkeys, and run instructions. + +## Where Things Are + +| Path | What it is | +|------|------------| +| `Sources/` | The SwiftUI app and `Config.plist.example` | +| `Quickstart.xcodeproj` | Xcode project, with the SDK as a remote Swift package | +| `Package.swift` | The same app as a Swift package | +| `thunderid-config/thunderid-config.yaml` | The application the [iOS SDK E2E suite](https://github.com/thunder-id/ios-sdks/tree/main/Tests/e2e) imports into ThunderID before it drives this sample | diff --git a/quickstarts/ios/Sources/App.swift b/quickstarts/ios/Sources/App.swift new file mode 100644 index 0000000..0e6bc94 --- /dev/null +++ b/quickstarts/ios/Sources/App.swift @@ -0,0 +1,34 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI +import ThunderIDSwiftUI + +@main +struct ThunderIDB2CApp: App { + private let config: ThunderIDConfig = { + guard let url = Bundle.main.url(forResource: "Config", withExtension: "plist"), + let dict = NSDictionary(contentsOf: url) as? [String: String] else { + fatalError( + "Config.plist not found β€” copy Config.plist.example to Sources/Config.plist and fill in your values" + ) + } + let attestationEnabled = (dict["THUNDERID_ATTESTATION_ENABLED"] ?? "").lowercased() == "true" + let attestationProvider = AppAttestTokenProvider() + return ThunderIDConfig( + baseUrl: dict["THUNDERID_BASE_URL"] ?? "", + scopes: ["openid", "profile", "email"], + applicationId: dict["THUNDERID_APPLICATION_ID"], + attestationEnabled: attestationEnabled, + attestationTokenProvider: attestationEnabled ? attestationProvider.requestToken : nil + ) + }() + + var body: some Scene { + WindowGroup { + RootView() + .thunderIDProvider(config: config) + .tint(Color(red: 0.212, green: 0.533, blue: 1.0)) + } + } +} diff --git a/quickstarts/ios/Sources/AppAttestTokenProvider.swift b/quickstarts/ios/Sources/AppAttestTokenProvider.swift new file mode 100644 index 0000000..db0f530 --- /dev/null +++ b/quickstarts/ios/Sources/AppAttestTokenProvider.swift @@ -0,0 +1,49 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import CryptoKit +import DeviceCheck +import Foundation + +/// Errors surfaced by ``AppAttestTokenProvider``. +enum AppAttestError: LocalizedError { + case unsupported + + var errorDescription: String? { + switch self { + case .unsupported: + return "App Attest is unavailable here β€” it requires a physical device with a Secure Enclave." + } + } +} + +/// Mints Apple App Attest tokens for `ThunderIDConfig.attestationTokenProvider`. +/// Requires a physical device and the App Attest entitlement. The challenge should +/// come from the server in production; this sample generates it locally. +final class AppAttestTokenProvider { + private let service = DCAppAttestService.shared + + /// Returns the base64-encoded App Attest attestation object, which is what the + /// `Attestation-Token` header carries. + /// + /// A key can be attested only once, and the server verifies a fresh attestation object on every + /// flow initiation, so this generates a new key per call rather than reusing a stored one. + func requestToken() async throws -> String { + guard service.isSupported else { throw AppAttestError.unsupported } + let keyId = try await service.generateKey() + let clientDataHash = Data(SHA256.hash(data: makeChallenge())) + let attestation = try await service.attestKey(keyId, clientDataHash: clientDataHash) + return attestation.base64EncodedString() + } + + /// Generates a random 32-byte challenge. + private func makeChallenge() -> Data { + var bytes = [UInt8](repeating: 0, count: 32) + let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) + guard status == errSecSuccess else { + var generator = SystemRandomNumberGenerator() + return Data((0.. + + + + THUNDERID_BASE_URL + https://localhost:8090 + THUNDERID_APPLICATION_ID + your-application-id + THUNDERID_ATTESTATION_ENABLED + false + + diff --git a/quickstarts/ios/Sources/HomeView.swift b/quickstarts/ios/Sources/HomeView.swift new file mode 100644 index 0000000..77f91e4 --- /dev/null +++ b/quickstarts/ios/Sources/HomeView.swift @@ -0,0 +1,411 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI +import ThunderIDSwiftUI + +// MARK: - App Screen Enum + +private enum AppScreen { + case home, profile, token +} + +// MARK: - HomeView + +struct HomeView: View { + @EnvironmentObject private var state: ThunderIDState + @Environment(\.colorScheme) private var colorScheme + + @State private var screen: AppScreen = .home + + private var isDark: Bool { colorScheme == .dark } + private var bgColor: Color { isDark ? Color(hex: "080f1c") : Color(hex: "F7F9FC") } + private var textColor: Color { isDark ? Color(hex: "E0EAFF") : Color(hex: "05213F") } + private var mutedColor: Color { isDark ? Color(hex: "E0EAFF").opacity(0.48) : Color(hex: "5A7085") } + private var borderColor: Color { isDark ? Color.white.opacity(0.09) : Color(hex: "DDE3EC") } + private var cardColor: Color { isDark ? Color(hex: "111c2e") : Color(hex: "ffffff") } + private var primaryBlue: Color { Color(hex: "3688FF") } + private var successGreen: Color { Color(hex: "2fbd6b") } + private var errorRed: Color { Color(hex: "d95757") } + + var body: some View { + NavigationStack { + ZStack { + bgColor.ignoresSafeArea() + switch screen { + case .home: + HomeScreen( + isDark: isDark, + bgColor: bgColor, + textColor: textColor, + mutedColor: mutedColor, + borderColor: borderColor, + cardColor: cardColor, + primaryBlue: primaryBlue, + successGreen: successGreen, + onProfile: { screen = .profile }, + onToken: { screen = .token } + ) + case .profile: + ProfileScreen( + isDark: isDark, + bgColor: bgColor, + textColor: textColor, + mutedColor: mutedColor, + borderColor: borderColor, + cardColor: cardColor, + primaryBlue: primaryBlue + ) { + screen = .home + } + case .token: + TokenScreen( + isDark: isDark, + bgColor: bgColor, + textColor: textColor, + mutedColor: mutedColor, + borderColor: borderColor, + cardColor: cardColor, + primaryBlue: primaryBlue, + successGreen: successGreen, + errorRed: errorRed + ) { + screen = .home + } + } + } + #if os(iOS) + .toolbar(.hidden, for: .navigationBar) + #endif + } + } +} + +// MARK: - Claim Decoding Helper + +/// Reads a unix-seconds numeric claim (`Int` or `Double`) from a decoded JWT/userinfo claims map. +func claimUnixSeconds(_ claim: Any?) -> TimeInterval? { + guard let value = claim else { return nil } + if let intValue = value as? Int { return TimeInterval(intValue) } + if let doubleValue = value as? Double { return doubleValue } + return nil +} + +// MARK: - Next Step Model + +private struct NextStep { + let number: String + let title: String + let subtitle: String + let href: String +} + +// MARK: - Home Screen + +private struct HomeScreen: View { + @EnvironmentObject private var state: ThunderIDState + let isDark: Bool + let bgColor: Color + let textColor: Color + let mutedColor: Color + let borderColor: Color + let cardColor: Color + let primaryBlue: Color + let successGreen: Color + let onProfile: () -> Void + let onToken: () -> Void + + private var greetingName: String { + if let given = state.user?["given_name"] as? String, !given.isEmpty { + return given + } + if let email = state.user?.email, let prefix = email.split(separator: "@").first, !prefix.isEmpty { + return String(prefix) + } + return "there" + } + + private var authTimeClaim: TimeInterval? { claimUnixSeconds(state.user?["auth_time"]) } + private var expClaim: TimeInterval? { claimUnixSeconds(state.user?["exp"]) } + + private var organisationName: String { + guard let handle = (try? state.client.getConfiguration())?.organizationHandle, !handle.isEmpty else { + return "Default" + } + return handle + } + + private var greeting: String { + let hour = Calendar.current.component(.hour, from: Date()) + let timeOfDay: String + if hour < 12 { + timeOfDay = "morning" + } else if hour < 17 { + timeOfDay = "afternoon" + } else { + timeOfDay = "evening" + } + return "Good \(timeOfDay), \(greetingName)." + } + + private var currentDateString: String { + let formatter = DateFormatter() + formatter.dateFormat = "EEEE, MMMM d" + return formatter.string(from: Date()).uppercased() + } + + private let nextSteps: [NextStep] = [ + NextStep( + number: "01", + title: "Explore use cases", + subtitle: "See what you can build β€” auth flows for web, mobile, APIs, and agents.", + href: "https://thunderid.dev/docs/next/use-cases/overview/" + ), + NextStep( + number: "02", + title: "Learn about flows", + subtitle: "Understand how authorization code, PKCE, client credentials, and device flows work.", + href: "https://thunderid.dev/docs/next/guides/flows/what-are-flows/" + ), + NextStep( + number: "03", + title: "Style your experience", + subtitle: "Customize the login UI, branding, and email templates to match your product.", + href: "https://thunderid.dev/docs/next/guides/design/overview/" + ), + NextStep( + number: "04", + title: "Explore SDK APIs", + subtitle: "Full iOS SDK reference β€” views, state objects, and configuration options.", + href: "https://thunderid.dev/docs/next/sdks/ios/overview/" + ) + ] + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + // Hero: avatar + (date/session row, greeting row) + heroSection + .padding(.horizontal, 24) + .padding(.top, 24) + + // Stats row + statsRow + .padding(.horizontal, 20) + .padding(.top, 20) + + // What's next section + sectionHeader("WHAT'S NEXT") + .padding(.horizontal, 24) + .padding(.top, 28) + + stepsCard + .padding(.horizontal, 20) + .padding(.top, 12) + + // User actions + sectionHeader("ACCOUNT") + .padding(.horizontal, 24) + .padding(.top, 28) + + actionsCard + .padding(.horizontal, 20) + .padding(.top, 12) + .padding(.bottom, 40) + } + } + .background(bgColor) + } + + private var heroSection: some View { + HStack(alignment: .center, spacing: 14) { + UserAvatar(size: 48) + + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 6) { + Text(currentDateString) + .font(.system(size: 11, weight: .semibold)) + .tracking(1.2) + .foregroundColor(mutedColor) + Circle() + .fill(mutedColor.opacity(0.4)) + .frame(width: 3, height: 3) + Circle() + .fill(successGreen) + .frame(width: 6, height: 6) + Text("Session active") + .font(.system(size: 11, weight: .medium)) + .foregroundColor(successGreen) + } + Text(greeting) + .font(.system(size: 22, weight: .bold)) + .foregroundColor(textColor) + } + } + } + + private var statsRow: some View { + TimelineView(.periodic(from: .now, by: 1)) { context in + HStack(spacing: 0) { + statItem(value: signedInAtText, label: "Signed in at") + Divider() + .frame(width: 1, height: 36) + .background(borderColor) + statItem(value: sessionExpiresInText(now: context.date), label: "Session expires in") + Divider() + .frame(width: 1, height: 36) + .background(borderColor) + statItem(value: organisationName, label: "Organisation") + } + .padding(.vertical, 16) + } + .background(cardColor) + .clipShape(RoundedRectangle(cornerRadius: 14)) + .overlay(RoundedRectangle(cornerRadius: 14).stroke(borderColor, lineWidth: 1)) + } + + private var signedInAtText: String { + guard let authTime = authTimeClaim else { return "β€”" } + let formatter = DateFormatter() + formatter.dateStyle = .none + formatter.timeStyle = .short + return formatter.string(from: Date(timeIntervalSince1970: authTime)) + } + + private func sessionExpiresInText(now: Date) -> String { + guard let exp = expClaim else { return "β€”" } + let secondsLeft = Int(exp - now.timeIntervalSince1970) + if secondsLeft <= 0 { return "Expired" } + if secondsLeft < 3600 { + return "\(secondsLeft / 60)m \(secondsLeft % 60)s" + } + return "\(secondsLeft / 3600)h \((secondsLeft % 3600) / 60)m" + } + + private func statItem(value: String, label: String) -> some View { + VStack(spacing: 3) { + Text(value) + .font(.system(size: 18, weight: .bold)) + .foregroundColor(textColor) + Text(label) + .font(.system(size: 11)) + .foregroundColor(mutedColor) + } + .frame(maxWidth: .infinity) + } + + private func sectionHeader(_ text: String) -> some View { + Text(text) + .font(.system(size: 11, weight: .semibold)) + .tracking(1.2) + .foregroundColor(mutedColor) + } + + @Environment(\.openURL) private var openURL + + private var stepsCard: some View { + VStack(spacing: 0) { + ForEach(Array(nextSteps.enumerated()), id: \.offset) { index, step in + if index > 0 { + Divider() + .background(borderColor) + .padding(.leading, 16) + } + Button { + guard let url = URL(string: step.href) else { return } + openURL(url) + } label: { + HStack(spacing: 14) { + Text(step.number) + .font(.system(size: 13, weight: .semibold, design: .monospaced)) + .foregroundColor(primaryBlue) + .frame(width: 28) + + VStack(alignment: .leading, spacing: 2) { + Text(step.title) + .font(.system(size: 14, weight: .bold)) + .foregroundColor(textColor) + Text(step.subtitle) + .font(.system(size: 12)) + .foregroundColor(mutedColor) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(mutedColor) + } + .padding(.horizontal, 16) + .padding(.vertical, 14) + } + .buttonStyle(.plain) + } + } + .background(cardColor) + .clipShape(RoundedRectangle(cornerRadius: 14)) + .overlay(RoundedRectangle(cornerRadius: 14).stroke(borderColor, lineWidth: 1)) + } + + private var actionsCard: some View { + VStack(spacing: 0) { + // Manage Account + Button(action: onProfile) { + actionRow(icon: "person.circle", label: "Manage Account", color: textColor) + } + + Divider() + .background(borderColor) + .padding(.leading, 52) + + // Token debug + Button(action: onToken) { + actionRow(icon: "key.horizontal", label: "Token debug", color: textColor) + } + + Divider() + .background(borderColor) + .padding(.leading, 52) + + // Sign out + signOutRow + } + .background(cardColor) + .clipShape(RoundedRectangle(cornerRadius: 14)) + .overlay(RoundedRectangle(cornerRadius: 14).stroke(borderColor, lineWidth: 1)) + } + + private func actionRow(icon: String, label: String, color: Color) -> some View { + HStack(spacing: 14) { + Image(systemName: icon) + .font(.system(size: 18)) + .foregroundColor(color) + .frame(width: 24) + Text(label) + .font(.system(size: 15)) + .foregroundColor(color) + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(mutedColor) + } + .padding(.horizontal, 16) + .padding(.vertical, 14) + } + + private var signOutRow: some View { + HStack(spacing: 14) { + Image(systemName: "rectangle.portrait.and.arrow.right") + .font(.system(size: 18)) + .foregroundColor(Color(hex: "d95757")) + .frame(width: 24) + SignOutButton() + .tint(Color(hex: "d95757")) + Spacer() + } + .padding(.horizontal, 16) + .padding(.vertical, 14) + } +} + +// Color(hex:) is defined in SignInView.swift and shared across the module. diff --git a/quickstarts/ios/Sources/HomeViewComponents.swift b/quickstarts/ios/Sources/HomeViewComponents.swift new file mode 100644 index 0000000..ae6eea1 --- /dev/null +++ b/quickstarts/ios/Sources/HomeViewComponents.swift @@ -0,0 +1,167 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI +import ThunderIDSwiftUI + +// MARK: - Shared Components + +struct UserAvatarView: View { + let user: User? + let radius: CGFloat + + var body: some View { + if let urlString = pictureUrl, let url = URL(string: urlString) { + AsyncImage(url: url) { image in + image.resizable().scaledToFill() + } placeholder: { + Color.accentColor.opacity(0.3) + } + .frame(width: radius * 2, height: radius * 2) + .clipShape(Circle()) + } else { + Circle() + .fill(Color.accentColor.opacity(0.15)) + .frame(width: radius * 2, height: radius * 2) + .overlay( + Text(initial) + .font(.system(size: radius * 0.9, weight: .bold)) + .foregroundColor(.accentColor) + ) + } + } + + private var pictureUrl: String? { + if let pic = user?["picture"] as? String, !pic.isEmpty { return pic } + return nil + } + + private var initial: String { + if let name = user?.displayName, !name.isEmpty { return String(name.prefix(1)).uppercased() } + if let email = user?.email, !email.isEmpty { return String(email.prefix(1)).uppercased() } + return "?" + } +} + +struct StatItemView: View { + let value: String + let label: String + + var body: some View { + VStack(spacing: 4) { + Text(value) + .font(.title2).bold() + Text(label) + .font(.caption) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) + } +} + +struct FeatureCardView: View { + let label: String + let imageUrl: String + let isNew: Bool + + var body: some View { + ZStack { + AsyncImage(url: URL(string: imageUrl)) { image in + image.resizable().scaledToFill() + } placeholder: { + Color.secondary.opacity(0.12) + } + .clipped() + + LinearGradient( + colors: [.clear, .black.opacity(0.6)], + startPoint: .top, + endPoint: .bottom + ) + + VStack(alignment: .leading) { + if isNew { + Text("NEW") + .font(.system(size: 10, weight: .bold)) + .foregroundColor(.white) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.accentColor) + .cornerRadius(12) + } + Spacer() + Text(label) + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.white) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(12) + } + .aspectRatio(1, contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(Color.gray.opacity(0.3), lineWidth: 1) + ) + } +} + +// MARK: - Listing Data & Card + +struct Listing { + let title: String + let location: String + let price: Int + let rating: Double + let imageUrl: String +} + +struct ListingCardView: View { + let listing: Listing + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + ZStack(alignment: .topTrailing) { + AsyncImage(url: URL(string: listing.imageUrl)) { image in + image.resizable().scaledToFill() + } placeholder: { + Color.secondary.opacity(0.12) + } + .frame(height: 130) + .clipped() + + Image(systemName: "heart") + .foregroundColor(.white) + .padding(8) + } + .frame(height: 130) + + VStack(alignment: .leading, spacing: 2) { + HStack { + Text(listing.location) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(1) + Spacer() + Image(systemName: "star.fill") + .font(.system(size: 9)) + .foregroundColor(.accentColor) + Text(String(format: "%.2f", listing.rating)) + .font(.caption) + } + Text(listing.title) + .font(.caption) + .fontWeight(.semibold) + .lineLimit(2) + Text("$\(listing.price)/night") + .font(.caption) + } + .padding(.horizontal, 8) + .padding(.vertical, 10) + } + .background(.background) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .shadow(color: .black.opacity(0.08), radius: 4, x: 0, y: 2) + } +} diff --git a/quickstarts/ios/Sources/ProfileView.swift b/quickstarts/ios/Sources/ProfileView.swift new file mode 100644 index 0000000..68ba24f --- /dev/null +++ b/quickstarts/ios/Sources/ProfileView.swift @@ -0,0 +1,51 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI +import ThunderIDSwiftUI + +// MARK: - Profile Screen + +struct ProfileScreen: View { + let isDark: Bool + let bgColor: Color + let textColor: Color + let mutedColor: Color + let borderColor: Color + let cardColor: Color + let primaryBlue: Color + let onBack: () -> Void + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + // Back nav + Button(action: onBack) { + HStack(spacing: 4) { + Image(systemName: "chevron.left") + .font(.system(size: 14, weight: .semibold)) + Text("Home") + .font(.system(size: 16)) + } + .foregroundColor(primaryBlue) + } + .padding(.horizontal, 20) + .padding(.top, 20) + .padding(.bottom, 8) + + // UserProfile owns the /users/me data, edit/save state, and its own styling + // (light/dark-adaptive), so this screen only supplies the surrounding chrome. + UserProfile() + .padding(.horizontal, 20) + .padding(.bottom, 24) + + // ChangeCredential manages the credential declared on the user type schema; its + // heading and labels follow that credential's own display name from the console. + ChangeCredential() + .padding(.horizontal, 20) + .padding(.bottom, 40) + } + } + .background(bgColor) + } +} diff --git a/quickstarts/ios/Sources/Quickstart.entitlements b/quickstarts/ios/Sources/Quickstart.entitlements new file mode 100644 index 0000000..6ba0b71 --- /dev/null +++ b/quickstarts/ios/Sources/Quickstart.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.associated-domains + + webcredentials:your-thunderid-domain.example + + + diff --git a/quickstarts/ios/Sources/RootView.swift b/quickstarts/ios/Sources/RootView.swift new file mode 100644 index 0000000..15c2a5b --- /dev/null +++ b/quickstarts/ios/Sources/RootView.swift @@ -0,0 +1,31 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI +import ThunderIDSwiftUI + +struct RootView: View { + @EnvironmentObject private var state: ThunderIDState + + var body: some View { + if !state.isInitialized || state.isLoading { + VStack(spacing: 12) { + ProgressView() + Text("Loading\u{2026}") + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if let error = state.error { + VStack(spacing: 16) { + Text("Configuration error: \(error)\n\nCheck your .env values.") + .multilineTextAlignment(.center) + .padding(24) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if state.isSignedIn { + HomeView() + } else { + AuthView() + } + } +} diff --git a/quickstarts/ios/Sources/SignInView.swift b/quickstarts/ios/Sources/SignInView.swift new file mode 100644 index 0000000..6ff2c9b --- /dev/null +++ b/quickstarts/ios/Sources/SignInView.swift @@ -0,0 +1,354 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI +import ThunderIDSwiftUI + +// MARK: - Color Helpers + +extension Color { + init(hex: String) { + let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int: UInt64 = 0 + Scanner(string: hex).scanHexInt64(&int) + let alpha, red, green, blue: UInt64 + switch hex.count { + case 3: + (alpha, red, green, blue) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: + (alpha, red, green, blue) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: + (alpha, red, green, blue) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (alpha, red, green, blue) = (255, 0, 0, 0) + } + self.init( + .sRGB, + red: Double(red) / 255, + green: Double(green) / 255, + blue: Double(blue) / 255, + opacity: Double(alpha) / 255 + ) + } +} + +// MARK: - ThunderID Logo Mark + +private struct TdMarkView: View { + let height: CGFloat + let dark: Bool + + private var width: CGFloat { height * (207.0 / 257.0) } + + var body: some View { + Canvas { context, size in + let scaleX = size.width / 207.0 + let scaleY = size.height / 257.0 + + func scaledPath(_ pathData: Path) -> Path { + pathData.applying(CGAffineTransform(scaleX: scaleX, y: scaleY)) + } + + // Path 1: wordmark top-left bar β€” text color + var path1 = Path() + path1.move(to: CGPoint(x: 55.4763, y: 26.4391)) + path1.addLine(to: CGPoint(x: 58.8866, y: 0)) + path1.addLine(to: CGPoint(x: 0, y: 0)) + path1.addLine(to: CGPoint(x: 0, y: 26.4391)) + path1.closeSubpath() + context.fill(scaledPath(path1), with: .color(dark ? Color(hex: "E8F4FF") : Color(hex: "05213F"))) + + // Path 2: left bar β€” blue + var path2 = Path() + path2.move(to: CGPoint(x: 39.8438, y: 147.407)) + path2.addLine(to: CGPoint(x: 49.5455, y: 72.2839)) + path2.addLine(to: CGPoint(x: 0, y: 72.2839)) + path2.addLine(to: CGPoint(x: 0, y: 256.743)) + path2.addLine(to: CGPoint(x: 60.5602, y: 256.743)) + path2.addLine(to: CGPoint(x: 80.048, y: 147.407)) + path2.closeSubpath() + context.fill(scaledPath(path2), with: .color(Color(hex: "3688FF"))) + + // Path 3: right bolt β€” blue + var path3 = Path() + path3.move(to: CGPoint(x: 192.42, y: 59.361)) + path3.addCurve( + to: CGPoint(x: 150.903, y: 15.3381), + control1: CGPoint(x: 182.782, y: 40.2307), + control2: CGPoint(x: 168.929, y: 25.5705) + ) + path3.addCurve( + to: CGPoint(x: 133.703, y: 7.5208), + control1: CGPoint(x: 145.501, y: 12.2662), + control2: CGPoint(x: 139.761, y: 9.6605) + ) + path3.addLine(to: CGPoint(x: 115.401, y: 103.702)) + path3.addLine(to: CGPoint(x: 159.757, y: 103.702)) + path3.addLine(to: CGPoint(x: 76.2987, y: 256.743)) + path3.addLine(to: CGPoint(x: 83.3735, y: 256.743)) + path3.addCurve( + to: CGPoint(x: 150.14, y: 241.236), + control1: CGPoint(x: 109.449, y: 256.743), + control2: CGPoint(x: 131.69, y: 251.574) + ) + path3.addCurve( + to: CGPoint(x: 192.356, y: 196.959), + control1: CGPoint(x: 168.569, y: 230.897), + control2: CGPoint(x: 182.634, y: 216.131) + ) + path3.addCurve( + to: CGPoint(x: 206.909, y: 128.043), + control1: CGPoint(x: 202.058, y: 177.765), + control2: CGPoint(x: 206.909, y: 154.8) + ) + path3.addCurve( + to: CGPoint(x: 192.441, y: 59.3821), + control1: CGPoint(x: 206.909, y: 101.286), + control2: CGPoint(x: 202.079, y: 78.5123) + ) + path3.closeSubpath() + context.fill(scaledPath(path3), with: .color(Color(hex: "3688FF"))) + } + .frame(width: width, height: height) + } +} + +// MARK: - AuthView (Landing Screen) + +struct AuthView: View { + @EnvironmentObject private var state: ThunderIDState + @Environment(\.colorScheme) private var colorScheme + + private var applicationId: String { + (try? state.client.getConfiguration())?.applicationId ?? "" + } + + @State private var showSignUp = false + @State private var showSignIn = false + @State private var showRecover = false + + private var isDark: Bool { colorScheme == .dark } + + private var bgColor: Color { isDark ? Color(hex: "080f1c") : Color(hex: "F7F9FC") } + private var textColor: Color { isDark ? Color(hex: "E0EAFF") : Color(hex: "05213F") } + private var mutedColor: Color { isDark ? Color(hex: "E0EAFF").opacity(0.48) : Color(hex: "5A7085") } + private var borderColor: Color { isDark ? Color.white.opacity(0.09) : Color(hex: "DDE3EC") } + private var cardColor: Color { isDark ? Color(hex: "111c2e") : Color(hex: "ffffff") } + private var primaryBlue: Color { Color(hex: "3688FF") } + + private let featureTags = ["OAuth 2.0", "PKCE", "JWT", "MFA", "SSO"] + + var body: some View { + ZStack { + bgColor.ignoresSafeArea() + + VStack(spacing: 0) { + Spacer() + + // Logo mark + TdMarkView(height: 64, dark: isDark) + .padding(.bottom, 32) + + // Headline + Text("Authentication\nfor developers.") + .font(.system(size: 28, weight: .bold)) + .tracking(-0.5) + .multilineTextAlignment(.center) + .foregroundColor(textColor) + .padding(.horizontal, 32) + + // Subtext + Text("OAuth 2.0, PKCE, MFA, and JWT\n- out of the box in minutes.") + .font(.system(size: 15)) + .multilineTextAlignment(.center) + .foregroundColor(mutedColor) + .padding(.top, 12) + .padding(.horizontal, 32) + + // Feature tags + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(featureTags, id: \.self) { tag in + Text(tag) + .font(.system(size: 12, weight: .medium)) + .foregroundColor(primaryBlue) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(primaryBlue.opacity(0.1)) + .overlay( + Capsule() + .stroke(primaryBlue.opacity(0.3), lineWidth: 1) + ) + .clipShape(Capsule()) + } + } + .padding(.horizontal, 24) + } + .padding(.top, 24) + + Spacer() + + // CTAs + VStack(spacing: 12) { + Button { + showSignUp = true + } label: { + Text("Get started") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 15) + .background(primaryBlue) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } + + Button { + showSignIn = true + } label: { + Text("Sign in") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(primaryBlue) + .frame(maxWidth: .infinity) + .padding(.vertical, 15) + .background(cardColor) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(primaryBlue, lineWidth: 1.5) + ) + } + } + .padding(.horizontal, 24) + .padding(.bottom, 48) + } + } + // Sign In sheet + .sheet(isPresented: $showSignIn) { + SignInSheet( + applicationId: applicationId, + isDark: isDark, + bgColor: bgColor, + textColor: textColor, + mutedColor: mutedColor, + primaryBlue: primaryBlue + ) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.visible) + } + // Sign Up sheet + .sheet(isPresented: $showSignUp) { + SignUpSheet( + applicationId: applicationId, + isDark: isDark, + bgColor: bgColor, + textColor: textColor, + mutedColor: mutedColor, + primaryBlue: primaryBlue + ) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.visible) + } + // Recover sheet + .sheet(isPresented: $showRecover) { + RecoverSheet( + isDark: isDark, + bgColor: bgColor, + textColor: textColor, + mutedColor: mutedColor, + primaryBlue: primaryBlue + ) { + showRecover = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { + showSignIn = true + } + } + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.visible) + } + } +} + +// MARK: - Sign In Sheet + +private struct SignInSheet: View { + let applicationId: String + let isDark: Bool + let bgColor: Color + let textColor: Color + let mutedColor: Color + let primaryBlue: Color + + var body: some View { + ZStack { + bgColor.ignoresSafeArea() + + VStack(alignment: .leading, spacing: 0) { + SignIn(applicationId: applicationId) + .padding(.horizontal, 24) + .padding(.top, 32) + .padding(.bottom, 24) + + Spacer() + } + } + } +} + +// MARK: - Sign Up Sheet + +private struct SignUpSheet: View { + let applicationId: String + let isDark: Bool + let bgColor: Color + let textColor: Color + let mutedColor: Color + let primaryBlue: Color + + var body: some View { + ZStack { + bgColor.ignoresSafeArea() + + VStack(alignment: .leading, spacing: 0) { + SignUp(applicationId: applicationId) + .padding(.horizontal, 24) + .padding(.top, 32) + .padding(.bottom, 24) + + Spacer() + } + } + } +} + +// MARK: - Recover Sheet + +private struct RecoverSheet: View { + let isDark: Bool + let bgColor: Color + let textColor: Color + let mutedColor: Color + let primaryBlue: Color + let onBackToSignIn: () -> Void + + var body: some View { + ZStack { + bgColor.ignoresSafeArea() + + VStack(alignment: .leading, spacing: 0) { + Spacer() + + Button(action: onBackToSignIn) { + HStack(spacing: 6) { + Image(systemName: "chevron.left") + .font(.system(size: 13, weight: .semibold)) + Text("Back to sign in") + .font(.system(size: 14, weight: .semibold)) + } + .foregroundColor(primaryBlue) + } + .frame(maxWidth: .infinity) + .padding(.bottom, 32) + } + } + } +} diff --git a/quickstarts/ios/Sources/TokenDebugView.swift b/quickstarts/ios/Sources/TokenDebugView.swift new file mode 100644 index 0000000..1ea235f --- /dev/null +++ b/quickstarts/ios/Sources/TokenDebugView.swift @@ -0,0 +1,279 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI +import ThunderIDSwiftUI + +// MARK: - Token Parts + +private struct TokenParts { + let header: String + let payload: String + let signature: String +} + +// MARK: - Token Screen + +struct TokenScreen: View { + @EnvironmentObject private var state: ThunderIDState + let isDark: Bool + let bgColor: Color + let textColor: Color + let mutedColor: Color + let borderColor: Color + let cardColor: Color + let primaryBlue: Color + let successGreen: Color + let errorRed: Color + let onBack: () -> Void + + @State private var accessToken: String = "" + @State private var isLoadingToken = true + @State private var tokenError: String? + @State private var copiedToken = false + + private var tokenParts: TokenParts? { + guard !accessToken.isEmpty else { return nil } + let parts = accessToken.split(separator: ".").map(String.init) + guard parts.count == 3 else { return nil } + return TokenParts(header: parts[0], payload: parts[1], signature: parts[2]) + } + + private var decodedPayload: [String: Any] { + decodeJwtPayload(accessToken) + } + + private var prettyPayload: String { + guard !decodedPayload.isEmpty else { return "{}" } + let options: JSONSerialization.WritingOptions = [.prettyPrinted, .sortedKeys] + if let data = try? JSONSerialization.data(withJSONObject: decodedPayload, options: options), + let str = String(data: data, encoding: .utf8) { + return str + } + return "{}" + } + + private var expiryInfo: (label: String, isExpired: Bool)? { + guard let exp = decodedPayload["exp"] as? Double else { return nil } + let expDate = Date(timeIntervalSince1970: exp) + let now = Date() + if expDate < now { return ("Expired", true) } + let minutes = Int(expDate.timeIntervalSince(now) / 60) + return ("\(minutes) min", false) + } + + private var issuer: String { + (decodedPayload["iss"] as? String) ?? "β€”" + } + + private var scopes: String { + if let scopeValue = decodedPayload["scope"] as? String { return scopeValue } + if let scopeList = decodedPayload["scp"] as? [String] { return scopeList.joined(separator: " ") } + return "β€”" + } + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + // Back nav + Button(action: onBack) { + HStack(spacing: 4) { + Image(systemName: "chevron.left") + .font(.system(size: 14, weight: .semibold)) + Text("Home") + .font(.system(size: 16)) + } + .foregroundColor(primaryBlue) + } + .padding(.horizontal, 20) + .padding(.top, 20) + + Text("Token Debug") + .font(.system(size: 26, weight: .bold)) + .foregroundColor(textColor) + .padding(.horizontal, 24) + .padding(.top, 8) + + if isLoadingToken { + HStack { + Spacer() + ProgressView() + .tint(primaryBlue) + Spacer() + } + .padding(.top, 48) + } else if let error = tokenError { + Text("Error: \(error)") + .font(.system(size: 14)) + .foregroundColor(errorRed) + .padding(.horizontal, 24) + .padding(.top, 20) + } else { + tokenCard + .padding(.horizontal, 20) + .padding(.top, 20) + + payloadCard + .padding(.horizontal, 20) + .padding(.top, 16) + + metaCard + .padding(.horizontal, 20) + .padding(.top, 16) + .padding(.bottom, 40) + } + } + } + .background(bgColor) + .task { + await loadToken() + } + } + + private func loadToken() async { + isLoadingToken = true + tokenError = nil + do { + accessToken = try await state.client.getAccessToken() + } catch { + tokenError = error.localizedDescription + } + isLoadingToken = false + } + + private var tokenCard: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Text("Access token") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(textColor) + Spacer() + Button(action: copyToken) { + HStack(spacing: 4) { + Image(systemName: copiedToken ? "checkmark" : "doc.on.doc") + .font(.system(size: 12)) + Text(copiedToken ? "Copied" : "Copy") + .font(.system(size: 12, weight: .medium)) + } + .foregroundColor(copiedToken ? successGreen : primaryBlue) + } + } + + if let parts = tokenParts { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 0) { + Text(parts.header) + .foregroundColor(Color(hex: "ff7b72")) + Text(".") + .foregroundColor(Color.white.opacity(0.4)) + Text(parts.payload) + .foregroundColor(Color(hex: "79c0ff")) + Text(".") + .foregroundColor(Color.white.opacity(0.4)) + Text(parts.signature) + .foregroundColor(Color(hex: "3fb950")) + } + .font(.system(size: 11, design: .monospaced)) + .padding(12) + } + .background(Color(hex: "0b1120")) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + } + .padding(16) + .background(cardColor) + .clipShape(RoundedRectangle(cornerRadius: 14)) + .overlay(RoundedRectangle(cornerRadius: 14).stroke(borderColor, lineWidth: 1)) + } + + private func copyToken() { + #if canImport(UIKit) + UIPasteboard.general.string = accessToken + #endif + copiedToken = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + copiedToken = false + } + } + + private var payloadCard: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Text("JWT Payload") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(textColor) + Spacer() + if let expiry = expiryInfo { + Text(expiry.label) + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(expiry.isExpired ? errorRed : successGreen) + .padding(.horizontal, 9) + .padding(.vertical, 4) + .background((expiry.isExpired ? errorRed : successGreen).opacity(0.12)) + .clipShape(Capsule()) + } + } + + ScrollView(.horizontal, showsIndicators: false) { + Text(prettyPayload) + .font(.system(size: 11, design: .monospaced)) + .foregroundColor(isDark ? Color(hex: "E0EAFF") : Color(hex: "05213F")) + .padding(12) + .frame(minWidth: 0, alignment: .leading) + } + .background(isDark ? Color(hex: "0b1120") : Color(hex: "DDE3EC").opacity(0.5)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + .padding(16) + .background(cardColor) + .clipShape(RoundedRectangle(cornerRadius: 14)) + .overlay(RoundedRectangle(cornerRadius: 14).stroke(borderColor, lineWidth: 1)) + } + + private var metaCard: some View { + VStack(spacing: 0) { + metaRow(label: "Issuer", value: issuer) + Divider() + .background(borderColor) + .padding(.leading, 16) + metaRow(label: "Scopes", value: scopes) + } + .background(cardColor) + .clipShape(RoundedRectangle(cornerRadius: 14)) + .overlay(RoundedRectangle(cornerRadius: 14).stroke(borderColor, lineWidth: 1)) + } + + private func metaRow(label: String, value: String) -> some View { + HStack(alignment: .top, spacing: 12) { + Text(label) + .font(.system(size: 13)) + .foregroundColor(mutedColor) + .frame(width: 60, alignment: .leading) + Text(value) + .font(.system(size: 13, design: .monospaced)) + .foregroundColor(textColor) + .lineLimit(3) + .multilineTextAlignment(.leading) + Spacer() + } + .padding(.horizontal, 16) + .padding(.vertical, 14) + } +} + +// MARK: - JWT Decode Helper + +private func decodeJwtPayload(_ token: String) -> [String: Any] { + let parts = token.split(separator: ".").map(String.init) + guard parts.count == 3 else { return [:] } + var base64 = parts[1] + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + while base64.count % 4 != 0 { base64 += "=" } + guard let data = Data(base64Encoded: base64), + let obj = try? JSONSerialization.jsonObject(with: data), + let dict = obj as? [String: Any] else { + return [:] + } + return dict +} diff --git a/quickstarts/ios/thunderid-config/thunderid-config.yaml b/quickstarts/ios/thunderid-config/thunderid-config.yaml new file mode 100644 index 0000000..94fc982 --- /dev/null +++ b/quickstarts/ios/thunderid-config/thunderid-config.yaml @@ -0,0 +1,34 @@ +--- +resource_type: application +# Application used exclusively by the iOS Quickstart E2E suite. It has no OAuth profile: the +# mobile SDKs authenticate app-natively via the Flow Execution API, so the sample app needs only +# the base URL and this application ID. +# +# The ID is fixed so the sample's Config.plist and the Maestro flows can be committed against a +# known value instead of discovering it at run time. +id: 019e5b10-1001-7a2b-9c3d-4e5f60718293 +ouHandle: default +name: Mobile Quickstart E2E App +description: Application for the ThunderID mobile SDK quickstart E2E suites +type: mobile +# A mobile application must normally prove its binary identity via platform attestation before it +# may initiate a flow directly, otherwise /flow/execute fails with FES-1016. Apple App Attest is +# unavailable in the iOS Simulator and Play Integrity requires real Play Services, so neither can +# be satisfied on the emulated devices this suite drives. devMode skips that check. +# +# Test-only. Never enable this on a real tenant: it removes the guarantee that the client +# initiating a flow is the genuine, unmodified app. +# +# NOTE: POST /import currently drops this block silently (it reports success, then stores +# attestation: null), so run-e2e.sh re-applies it over PUT /applications/{id}. Once the +# import path preserves it, that extra step can go. +attestation: + devMode: true +authFlowHandle: default-flow +# The suite registers a fresh user through the UI rather than relying on a seeded account, so +# self-service registration has to be enabled for this application. +registrationFlowHandle: default-flow +isRegistrationFlowEnabled: true +signOutFlowHandle: default-flow +allowedUserTypes: + - Person diff --git a/quickstarts/nextjs/.env.example b/quickstarts/nextjs/.env.example new file mode 100644 index 0000000..66cdadd --- /dev/null +++ b/quickstarts/nextjs/.env.example @@ -0,0 +1,20 @@ +# Base URL of your ThunderID deployment. ex: https://localhost:8090 +NEXT_PUBLIC_THUNDERID_BASE_URL=https://localhost:8090 + +# Flow Secret for this app. Sent in the `Flow-Secret` header to authenticate this app when the +# native (embedded) flow starts. Shown once when the application is created; if lost, regenerate it +# from the app's Credentials tab (under Edit). Server-only, never expose to the browser. +THUNDERID_FLOW_SECRET=your-flow-secret-here + +# Secret used to encrypt this app's session cookie. Generate locally, not from the console. +THUNDERID_SECRET=generate-with-openssl-rand-base64-32 + +# Application ID (spId) for this app. ThunderID Console -> your application -> Overview. +NEXT_PUBLIC_THUNDERID_APPLICATION_ID=your-application-id-here +# Local app route that renders the sign-in page. Not from the console. +NEXT_PUBLIC_THUNDERID_SIGN_IN_URL=/signin +# Local app route that renders the sign-up page. Not from the console. +NEXT_PUBLIC_THUNDERID_SIGN_UP_URL=/signup + +# 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/quickstarts/nextjs/.gitignore b/quickstarts/nextjs/.gitignore new file mode 100644 index 0000000..304f800 --- /dev/null +++ b/quickstarts/nextjs/.gitignore @@ -0,0 +1,3 @@ +node_modules +.next +.env diff --git a/quickstarts/nextjs/.stackblitzrc b/quickstarts/nextjs/.stackblitzrc new file mode 100644 index 0000000..5f0a200 --- /dev/null +++ b/quickstarts/nextjs/.stackblitzrc @@ -0,0 +1,4 @@ +{ + "installDependencies": false, + "startCommand": "npm run prepare-dev && npm install && npm run dev" +} diff --git a/quickstarts/nextjs/README.md b/quickstarts/nextjs/README.md new file mode 100644 index 0000000..afffece --- /dev/null +++ b/quickstarts/nextjs/README.md @@ -0,0 +1,117 @@ +# ThunderID Next.js Quickstart + +Open in StackBlitz + +A minimal Next.js 15 App Router application demonstrating ThunderID authentication with OAuth 2.0, PKCE, and JWT out of the box. + +## Prerequisites + +- Node.js 18+ +- pnpm +- A ThunderID application + +## Getting started + +1. Copy the example environment file: + + ```bash + cp .env.example .env + ``` + +2. Fill in your ThunderID credentials in `.env`. By default the file is set up for the native flow: + + ```dotenv + NEXT_PUBLIC_THUNDERID_BASE_URL=https://localhost:8090 + NEXT_PUBLIC_THUNDERID_APPLICATION_ID= + THUNDERID_FLOW_SECRET= + NEXT_PUBLIC_THUNDERID_SIGN_IN_URL=/signin + NEXT_PUBLIC_THUNDERID_SIGN_UP_URL=/signup + THUNDERID_SECRET= + ``` + + The Flow Secret is generated by the server when the application is created, shown once, and only + retrievable afterward from the app's Credentials tab (under Edit). It's a separate credential from + the OAuth2 Client Secret used by the redirect-based flow below β€” an embedded app (no OAuth2 + configuration) has a Flow Secret and no client secret; a redirect-based `authorization_code` app has + a client secret and no Flow Secret. + +3. Start the development server: + + ```bash + pnpm dev + ``` + + The app is now running at [http://localhost:3000](http://localhost:3000). + +
    +

    Redirect-based flow

    + +By default this quickstart uses the native (embedded) flow, where sign-in/sign-up render inline on this +app's own `/signin` and `/signup` routes with no redirect to ThunderID's hosted pages. + +To send the user to ThunderID's hosted sign-in page instead, switch to the redirect-based flow: + +1. Register a redirect URI for your application (see the app's config notice in the console for the + exact value to use). +2. Regenerate `.env` for the redirect flow: + + ```bash + npm run prepare-dev:redirect + ``` + + This comments out the native-flow vars (`NEXT_PUBLIC_THUNDERID_APPLICATION_ID`, + `NEXT_PUBLIC_THUNDERID_SIGN_IN_URL`, `NEXT_PUBLIC_THUNDERID_SIGN_UP_URL`, `THUNDERID_FLOW_SECRET`) and + adds: + + ```dotenv + NEXT_PUBLIC_THUNDERID_CLIENT_ID= + THUNDERID_CLIENT_SECRET= + ``` + + Both values come from the application's Credentials tab in the console. To switch back to the native + flow, run `node scripts/prepare-dev.cjs --flow=native` (or manually re-enable the native-flow vars and + comment out the two above). +3. Fill in `NEXT_PUBLIC_THUNDERID_CLIENT_ID` and `THUNDERID_CLIENT_SECRET` in `.env`, then restart the dev + server. + +
    + +## Back-channel logout + +ThunderID can end this app's session itself. When the user's ThunderID session ends somewhere else, +for example by signing out of another application, ThunderID posts a logout token to this app. The +route is one line in `app/api/auth/backchannel-logout/route.ts`: + +```ts +export const {POST} = backchannelLogout() +``` + +The session lives in a cookie that only the browser can delete, so the SDK records the logout and +treats every later request with that session as signed out. + +To try it, use the redirect-based flow above, since the logout token is addressed to the client ID: + +1. In the ThunderID Console, open this application, go to **Advanced Settings β†’ OAuth2 + Configuration**, and set **Back-Channel Logout URI** to + `http://localhost:3000/api/auth/backchannel-logout`. +2. ThunderID refuses localhost and private network addresses by default. For local testing only, + turn that off in the server's `deployment.yaml` and restart it: + ```yaml + oauth: + logout: + backchannel: + reject_private_addresses: false + ``` +3. Sign in at [http://localhost:3000](http://localhost:3000), then sign in to a second application + in the same browser, so both share one ThunderID session. +4. Sign out of the second application, then reload this app: you are signed out here too. + +Things to know before production: + +- The SDK records ended sessions in memory by default. If you run more than one instance, pass a + shared `store` to both `backchannelLogout()` and `thunderIDProxy()`, since ThunderID's request + reaches only one instance. +- Route protection in `thunderIDProxy()` sees the in-memory record only when the proxy runs on the + Node.js runtime. On Next.js 15, add `runtime: 'nodejs'` to the `config` export of your + `middleware.ts`. On the Edge runtime, use a shared `store`. +- An access token already handed to the browser or to another API stays valid until it expires. diff --git a/quickstarts/nextjs/app/account/page.tsx b/quickstarts/nextjs/app/account/page.tsx new file mode 100644 index 0000000..d0e17a4 --- /dev/null +++ b/quickstarts/nextjs/app/account/page.tsx @@ -0,0 +1,212 @@ +'use client' +import { ChangeCredential, SignedIn, SignedOut, User, UserProfile } from '@thunderid/nextjs' +import Link from 'next/link' +import { useRouter, useSearchParams } from 'next/navigation' +import { useState } from 'react' +import Nav from '../components/Nav' + +function HomeIcon() { + return ( + + ) +} + +function PersonIcon() { + return ( + + ) +} + +function ShieldIcon() { + return ( + + ) +} + +const TABS = [ + { id: 'home', label: 'Home', icon: HomeIcon }, + { id: 'personal', label: 'Personal info', icon: PersonIcon }, + { id: 'security', label: 'Security', icon: ShieldIcon }, +] + +const CREDENTIALS = [ + { attribute: 'password', title: 'Password', description: 'Used to sign in to your account.', cta: 'Change password' }, +] + +function ChevronIcon() { + return ( + + + + ) +} + +function CredentialCard({ attribute, title, description, cta }: { attribute: string; title: string; description: string; cta: string }) { + const [open, setOpen] = useState(false) + const close = () => setOpen(false) + + return ( +
    +
    +
    +
    {title}
    +
    {description}
    +
    + +
    + {open && ( +
    + +
    + )} +
    + ) +} + +const TILES = [ + { + id: 'personal', + icon: PersonIcon, + tone: 'blue', + title: 'Personal info', + description: 'Name, email, phone, and profile photo', + }, + { + id: 'security', + icon: ShieldIcon, + tone: 'green', + title: 'Security', + description: 'Password and other credentials', + }, +] + +// Google Account-style layout: a persistent left nav switching between tabs of +// content on the right, rather than a single scrolling page or a modal. +function AccountTabs() { + const router = useRouter() + const searchParams = useSearchParams() + const requestedTab = searchParams.get('tab') + const [tab, setTab] = useState(TABS.some((t) => t.id === requestedTab) ? requestedTab! : 'home') + + const selectTab = (id: string) => { + setTab(id) + router.replace(id === 'home' ? '/account' : `/account?tab=${id}`) + } + + return ( +
    + + +
    +
    + {tab === 'home' && ( + + {(user: { givenName?: string; given_name?: string; displayName?: string; username?: string } | null) => { + const givenName = user?.givenName ?? user?.given_name ?? user?.displayName ?? user?.username ?? 'there' + + return ( + <> +

    Hi, {givenName}

    +

    + Manage your info and security across ThunderID apps. +

    +
    + {TILES.map(({ id, icon: Icon, tone, title, description }) => ( + + ))} +
    + + ) + }} +
    + )} + + {tab === 'personal' && ( + <> +

    Personal info

    +

    Manage the basic profile info others may see.

    + + + )} + + {tab === 'security' && ( + <> +

    Security

    +

    Manage your password and other credentials.

    +
    + {CREDENTIALS.map((credential) => ( + + ))} +
    + + )} +
    +
    +
    + ) +} + +export default function AccountPage() { + return ( +
    +
    + ) +} diff --git a/quickstarts/nextjs/app/actions.ts b/quickstarts/nextjs/app/actions.ts new file mode 100644 index 0000000..6a72ed3 --- /dev/null +++ b/quickstarts/nextjs/app/actions.ts @@ -0,0 +1,19 @@ +'use server' + +import { thunderid } from '@thunderid/nextjs/server' + +/** + * Fetch the raw access token JWT for the current session. + * Tokens are kept server-side β€” this Server Action is the only way a + * client component can retrieve them without exposing the session cookie. + */ +export async function fetchAccessToken(): Promise { + try { + const sdk = await thunderid() + const sessionId = await sdk.getSessionId() + if (!sessionId) return undefined + return await sdk.getAccessToken(sessionId) + } catch { + return undefined + } +} diff --git a/quickstarts/nextjs/app/api/auth/backchannel-logout/route.ts b/quickstarts/nextjs/app/api/auth/backchannel-logout/route.ts new file mode 100644 index 0000000..c09cc0d --- /dev/null +++ b/quickstarts/nextjs/app/api/auth/backchannel-logout/route.ts @@ -0,0 +1,7 @@ +import {backchannelLogout} from '@thunderid/nextjs/server' + +// ThunderID posts a logout token here when the user's session ends somewhere else, such as a +// sign-out from another application. The SDK records the logout, and from then on this app treats +// the matching session cookie as signed out. Register this path as the application's +// Back-Channel Logout URI to use it (see the README). +export const {POST} = backchannelLogout() diff --git a/quickstarts/nextjs/app/components/ConfigNotice.tsx b/quickstarts/nextjs/app/components/ConfigNotice.tsx new file mode 100644 index 0000000..4d581cb --- /dev/null +++ b/quickstarts/nextjs/app/components/ConfigNotice.tsx @@ -0,0 +1,121 @@ +'use client' +import { useState } from 'react' +import ThunderMark from './ThunderMark' +import NextLogo from './icons/NextLogo' + +function MoonIcon() { + return ( + + + + ) +} + +function SunIcon() { + return ( + + + + + + + + ) +} + +export default function ConfigNotice({ missing, isRedirectFlow }: { missing: string[]; isRedirectFlow: boolean }) { + const [dark, setDark] = useState(false) + + const toggle = () => { + const next = !dark + setDark(next) + document.documentElement.classList.toggle('dark', next) + } + + return ( +
    + + +
    +
    +
    + +
    + +
    + + Setup required + +
    + +

    Configuration needed

    + +

    + This quickstart 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. +

    +
    + + {isRedirectFlow && ( +
    +
    Step 2 · Register redirect URIs
    + +
    +

    + Sign-in and sign-out are handled by this app's server, so + no CORS configuration is needed. In the{' '} + ThunderID Console, open this application and go + to Advanced Settings → OAuth2 Configuration, + then add the exact URIs below. +

    + +
    +
    +
    Authorized redirect URI
    + http://localhost:3000 +
    +
    +
    Post-Logout Redirect URI
    + http://localhost:3000 +
    +
    +
    +
    + )} + +

    + Need more info? Take a look at the{' '} + + Next.js quickstart guide. + +

    +
    +
    +
    + ) +} diff --git a/quickstarts/nextjs/app/components/HeroCtas.tsx b/quickstarts/nextjs/app/components/HeroCtas.tsx new file mode 100644 index 0000000..ea8e91b --- /dev/null +++ b/quickstarts/nextjs/app/components/HeroCtas.tsx @@ -0,0 +1,16 @@ +'use client' +import { SignInButton } from '@thunderid/nextjs' + +export default function HeroCtas() { + return ( +
    + + {({ signIn, isLoading }) => ( + + )} + +
    + ) +} diff --git a/quickstarts/nextjs/app/components/Nav.tsx b/quickstarts/nextjs/app/components/Nav.tsx new file mode 100644 index 0000000..e660f18 --- /dev/null +++ b/quickstarts/nextjs/app/components/Nav.tsx @@ -0,0 +1,102 @@ +'use client' +import { SignedIn, SignedOut, SignInButton, UserDropdown } from '@thunderid/nextjs' +import Link from 'next/link' +import { usePathname, useRouter } from 'next/navigation' +import { useState } from 'react' +import NextLogo from './icons/NextLogo' + +function MoonIcon() { + return ( + + + + ) +} + +function SunIcon() { + return ( + + + + + + + + ) +} + +function ChevronLeftIcon() { + return ( + + ) +} + +function KeyIcon() { + return ( + + ) +} + +export default function Nav() { + const [dark, setDark] = useState(false) + const pathname = usePathname() + const router = useRouter() + const isHome = pathname === '/' + + const toggle = () => { + const next = !dark + setDark(next) + document.documentElement.classList.toggle('dark', next) + } + + const menuItems = [ + { + label: Token debug, + href: '/token', + }, + ] + + return ( + + ) +} diff --git a/quickstarts/nextjs/app/components/ThunderMark.tsx b/quickstarts/nextjs/app/components/ThunderMark.tsx new file mode 100644 index 0000000..d8cf0ab --- /dev/null +++ b/quickstarts/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/quickstarts/nextjs/app/components/icons/NextLogo.tsx b/quickstarts/nextjs/app/components/icons/NextLogo.tsx new file mode 100644 index 0000000..b78a766 --- /dev/null +++ b/quickstarts/nextjs/app/components/icons/NextLogo.tsx @@ -0,0 +1,17 @@ +export default function NextLogo({ size = 16, fill = 'currentColor' }: { size?: number; fill?: string }) { + return ( + + ) +} diff --git a/quickstarts/nextjs/app/globals.css b/quickstarts/nextjs/app/globals.css new file mode 100644 index 0000000..6972b47 --- /dev/null +++ b/quickstarts/nextjs/app/globals.css @@ -0,0 +1,1521 @@ +/* ── 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); + + /* ThunderID Design Tokens for Dark Mode */ + --thunderid-color-background-surface: #0f1a2d; + --thunderid-color-background-body: #080f1c; + --thunderid-color-background-disabled: rgba(255, 255, 255, 0.06); + --thunderid-color-background-muted: rgba(255, 255, 255, 0.05); + --thunderid-color-text-primary: #E0EAFF; + --thunderid-color-text-secondary: rgba(224, 234, 255, 0.65); + --thunderid-color-border: rgba(255, 255, 255, 0.09); + --thunderid-color-border-focus: var(--color-primary); + --thunderid-input-borderColor: rgba(255, 255, 255, 0.16); + --thunderid-input-focusBorderColor: var(--color-primary); + --thunderid-input-focusRing: 0 0 0 3px rgba(54, 136, 255, 0.25); + --thunderid-card-borderColor: rgba(255, 255, 255, 0.09); + --thunderid-card-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); + --thunderid-color-action-hover: rgba(255, 255, 255, 0.06); +} + +/* ── 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-brand { + display: flex; + align-items: center; + gap: 10px; + text-decoration: none; + color: var(--color-text); +} + +.nav-logo { + display: flex; + align-items: center; + gap: 10px; + text-decoration: none; + color: var(--color-text); +} + +.nav-wordmark { + font-size: 15px; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--color-text); +} + +.wordmark { + display: flex; + flex-direction: column; + gap: 1px; +} + +.wordmark-name { + font-size: 14px; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--color-text); + line-height: 1; +} + +.wordmark-sub { + font-size: 8.5px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-primary); + 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, +.btn-ghost { + 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-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); +} + +.btn-ghost { + background: transparent; + color: var(--color-muted); + border: none; +} + +.btn-ghost:hover { + color: var(--color-text); + background: rgba(0, 0, 0, 0.04); +} + +/* Nav-sized buttons */ +.nav .btn-primary, +.nav .btn-outline, +.nav .btn-ghost { + 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 / welcome card ── */ +.user-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + background: var(--color-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-card); + padding: 32px; + max-width: 480px; + width: 100%; + box-shadow: var(--shadow-card); +} + +.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; +} + +.user-welcome { + font-size: 1.375rem; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--color-text); + text-align: center; +} + +.user-details { + display: flex; + flex-direction: column; + gap: 0; + width: 100%; + border: 1px solid var(--color-border); + border-radius: 8px; + overflow: hidden; +} + +.user-detail-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 16px; + gap: 16px; +} + +.user-detail-row + .user-detail-row { + border-top: 1px solid var(--color-border); +} + +.user-detail-row dt { + font-size: 13px; + color: var(--color-text-muted); + font-weight: 500; + flex-shrink: 0; +} + +.user-detail-row dd { + font-size: 13px; + color: var(--color-text); + font-weight: 500; + text-align: right; + word-break: break-all; +} + +/* ── 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; +} + +/* ── Nav back button ── */ +.nav-back-btn { + display: inline-flex; + align-items: center; + gap: 4px; + height: 32px; + padding: 0 10px; + border-radius: 6px; + border: none; + background: transparent; + color: var(--color-muted); + font-size: 13px; + font-family: var(--font-sans); + letter-spacing: -0.01em; + cursor: pointer; + transition: background 0.2s, color 0.2s; +} + +.nav-back-btn:hover { + background: var(--color-border); + color: var(--color-text); +} + +/* ── Token debug page ── */ +.token-main { + width: 100%; + max-width: 780px; + margin: 0 auto; + margin-top: 52px; + padding: 52px 28px 80px; +} + +.token-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 36px; + flex-wrap: wrap; +} + +.token-title { + margin: 0 0 5px; + font-size: 22px; + font-weight: 700; + letter-spacing: -0.04em; + color: var(--color-text); +} + +.token-subtitle { + margin: 0; + font-size: 13px; + color: var(--color-muted); + letter-spacing: -0.01em; +} + +/* ── Sign in / Sign up pages ── */ +.auth-main { + width: 100%; + min-height: calc(100vh - 52px); + margin-top: 52px; + display: flex; + align-items: center; + justify-content: center; + padding: 40px 20px; +} + +.auth-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + background: var(--color-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-card); + box-shadow: var(--shadow-card); + padding: 40px 32px; + max-width: 420px; + width: 100%; +} + +.auth-title { + font-size: 1.5rem; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--color-text); + text-align: center; +} + +.auth-subtitle { + margin: 0 0 16px; + font-size: 13px; + color: var(--color-text-muted); + text-align: center; +} + +.auth-switch { + margin-top: 20px; + font-size: 13px; + color: var(--color-text-muted); + text-align: center; +} + +.auth-switch a { + color: var(--color-primary); + font-weight: 600; + text-decoration: none; +} + +.auth-switch a:hover { + text-decoration: underline; +} + +.token-badge { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 6px 12px; + border-radius: 6px; + font-size: 12px; + font-weight: 700; + letter-spacing: -0.01em; + flex-shrink: 0; +} + +.token-badge--expired { + background: rgba(220, 38, 38, 0.08); + border: 1px solid rgba(220, 38, 38, 0.22); + color: #dc2626; +} + +.token-badge--valid { + background: rgba(47, 189, 107, 0.08); + border: 1px solid rgba(47, 189, 107, 0.22); + color: #2fbd6b; +} + +.token-badge-dot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; + flex-shrink: 0; +} + +/* Raw token section */ +.token-raw-section { + margin-bottom: 20px; +} + +.token-raw-label-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.token-section-label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--color-muted); +} + +.token-copy-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 10px; + border-radius: 5px; + background: transparent; + border: 1px solid var(--color-border); + font-size: 11px; + font-weight: 600; + color: var(--color-muted); + cursor: pointer; + font-family: var(--font-sans); + letter-spacing: -0.01em; + transition: border-color 0.2s, color 0.2s; +} + +.token-copy-btn:hover { + border-color: #3688ff; + color: #3688ff; +} + +.token-raw { + display: block; + font-size: 11px; + font-family: monospace; + word-break: break-all; + line-height: 1.8; + background: #0b1120; + padding: 14px 16px; + border-radius: 8px; + overflow-x: auto; +} + +.token-raw--loading { + color: rgba(255, 255, 255, 0.3); +} + +.token-part--header { color: #ff7b72; } +.token-part--payload { color: #79c0ff; } +.token-part--signature { color: #3fb950; } + +.token-dot { + color: rgba(255, 255, 255, 0.2); +} + +/* Decoded grid */ +.token-decoded-grid { + display: grid; + grid-template-columns: 1fr 1.7fr; + gap: 12px; + margin-bottom: 20px; +} + +.token-code-box { + background: #0b1120; + border-radius: 10px; + overflow: hidden; +} + +.token-code-box-header { + padding: 9px 14px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.09em; + text-transform: uppercase; +} + +.token-code-box-header--header { + color: #ff7b72; + border-bottom: 1px solid rgba(255, 123, 114, 0.15); +} + +.token-code-box-header--payload { + color: #79c0ff; + border-bottom: 1px solid rgba(121, 192, 255, 0.15); +} + +.token-code-pre { + margin: 0; + padding: 14px; + font-size: 11.5px; + line-height: 1.8; + font-family: monospace; + color: #c9d1d9; + overflow-x: auto; + max-height: 220px; +} + +/* Meta row */ +.token-meta-row { + display: flex; + gap: 32px; + padding-top: 20px; + border-top: 1px solid var(--color-border); + flex-wrap: wrap; +} + +.token-meta-label { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--color-muted); + margin-bottom: 5px; +} + +.token-meta-value { + font-size: 12px; + font-family: monospace; + color: var(--color-text); + letter-spacing: -0.01em; +} + +/* ── Responsive ── */ +@media (max-width: 480px) { + .nav { + padding: 0 16px; + } + + .nav-wordmark { + display: none; + } + + .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; + } + + .user-card { + padding: 24px 16px; + } +} + +/* ─── Account page (Google Account-style left-nav layout) ─────────── */ +.account-page { + display: flex; + align-items: flex-start; + min-height: calc(100vh - 52px); + margin-top: 52px; +} + +.account-sidebar { + width: 280px; + flex-shrink: 0; + padding: 40px 24px; + position: sticky; + top: 52px; +} + +.account-sidebar-title { + font-size: 22px; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--color-text); + margin: 0 0 24px 16px; +} + +.account-nav { + display: flex; + flex-direction: column; + gap: 2px; +} + +.account-nav-item { + display: flex; + align-items: center; + gap: 14px; + width: 100%; + padding: 11px 16px; + border: none; + border-radius: 20px; + background: transparent; + color: var(--color-muted); + font-size: 14px; + font-weight: 500; + font-family: inherit; + text-align: left; + cursor: pointer; + white-space: nowrap; + transition: background 0.2s, color 0.2s; +} + +.account-nav-item:hover { + background: var(--color-border); + color: var(--color-text); +} + +.account-nav-item--active, +.account-nav-item--active:hover { + background: rgba(54, 136, 255, 0.12); + color: var(--color-primary); +} + +.account-content { + flex: 1; + min-width: 0; + display: flex; + justify-content: center; + padding: 40px 32px 80px; + border-left: 1px solid var(--color-border); +} + +.account-content-inner { + width: 100%; + max-width: 720px; +} + +/* UserProfile's `min-width: 600px` (a floor meant to keep the field grid from getting + cramped in other contexts) fights this page's own responsive layout, refusing to shrink + below 600px even when the viewport is narrower. The Security/Home boxes have no such floor + and already shrink fluidly with their container, so match that here. */ +.account-content .thunderid-user-profile { + min-width: 0; + background: var(--color-card) !important; + border: 1px solid var(--color-border) !important; + border-radius: var(--radius-card) !important; + box-shadow: var(--shadow-card); + color: var(--color-text); + box-sizing: border-box; +} + +.account-content .thunderid-user-profile .thunderid-user-profile__hero { + border-bottom: 1px solid var(--color-border); +} + +.account-content .thunderid-user-profile .thunderid-user-profile__hero-name { + color: var(--color-text); +} + +.account-content .thunderid-user-profile .thunderid-user-profile__hero-subtitle { + color: var(--color-text-muted); +} + +.account-content .thunderid-user-profile .thunderid-user-profile__field { + border-bottom: 1px solid var(--color-border); +} + +.account-content .thunderid-user-profile .thunderid-user-profile__field-label { + color: var(--color-text-muted); +} + +.account-content .thunderid-user-profile .thunderid-user-profile__field-value { + color: var(--color-text); +} + +.account-content .thunderid-user-profile .thunderid-user-profile__field-placeholder { + color: var(--color-text-muted); +} + +.account-content .thunderid-user-profile .thunderid-user-profile__field-edit-btn { + color: var(--color-text-muted); +} + +.account-content .thunderid-user-profile .thunderid-user-profile__field-edit-btn:hover { + color: var(--color-primary); +} + +.account-content .thunderid-user-profile input, +.account-content .thunderid-user-profile select, +.account-content .thunderid-user-profile textarea { + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--color-border); + border-radius: var(--radius-btn); + color: var(--color-text); + padding: 6px 10px; +} + +.account-content-title { + font-size: 26px; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--color-text); + margin: 0 0 6px; +} + +.account-content-subtitle { + font-size: 14px; + color: var(--color-muted); + margin: 0 0 32px; +} + +.account-tiles { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + margin-top: 8px; +} + +.account-tile { + text-align: left; + background: var(--color-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-card); + padding: 22px; + cursor: pointer; + font-family: inherit; + display: flex; + flex-direction: column; + transition: border-color 0.2s; +} + +.account-tile:hover { + border-color: rgba(54, 136, 255, 0.35); +} + +.account-tile-icon { + width: 36px; + height: 36px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 14px; +} + +.account-tile-icon--blue { + background: rgba(54, 136, 255, 0.12); + color: var(--color-primary); +} + +.account-tile-icon--green { + background: rgba(16, 185, 129, 0.12); + color: #10b981; +} + +.account-tile-title { + font-size: 15px; + font-weight: 600; + color: var(--color-text); + margin-bottom: 4px; +} + +.account-tile-desc { + font-size: 12.5px; + color: var(--color-muted); + line-height: 1.5; +} + +.account-security-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.account-security-card { + background: var(--color-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-card); + padding: 20px 22px; +} + +.account-security-card-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.account-security-card-title { + font-size: 14px; + font-weight: 600; + color: var(--color-text); + margin-bottom: 2px; +} + +.account-security-card-desc { + font-size: 12.5px; + color: var(--color-muted); +} + +.account-security-card-cta { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 6px; + padding: 0; + background: none; + border: none; + color: var(--color-primary); + font-size: 13px; + font-weight: 500; + font-family: inherit; + cursor: pointer; + transition: opacity 0.2s; +} + +.account-security-card-cta:hover { + opacity: 0.8; +} + +.account-security-card-chevron { + display: inline-flex; + transition: transform 0.2s; +} + +.account-security-card-chevron--open { + transform: rotate(180deg); +} + +.account-security-card-form { + margin-top: 18px; + padding-top: 18px; + border-top: 1px solid var(--color-border); +} + +@media (max-width: 760px) { + .account-page { + flex-direction: column; + } + .account-sidebar { + width: 100%; + position: static; + padding: 20px 16px 8px; + } + .account-nav { + flex-direction: row; + overflow-x: auto; + } + .account-content { + max-width: 100%; + border-left: none; + border-top: 1px solid var(--color-border); + padding: 24px 20px 60px; + } +} + +/* ── ThunderID inputs & forms dark mode ── */ +.dark .thunderid-password-field__wrapper, +html.dark .thunderid-password-field__wrapper, +.app.dark .thunderid-password-field__wrapper, +.dark .thunderid-text-field__wrapper, +html.dark .thunderid-text-field__wrapper, +.app.dark .thunderid-text-field__wrapper, +.dark .thunderid-text-field__input, +html.dark .thunderid-text-field__input, +.app.dark .thunderid-text-field__input { + background-color: rgba(255, 255, 255, 0.05) !important; + border-color: rgba(255, 255, 255, 0.16) !important; + color: var(--color-text) !important; +} + +.dark .thunderid-password-field__label, +html.dark .thunderid-password-field__label, +.app.dark .thunderid-password-field__label, +.dark .thunderid-text-field__label, +html.dark .thunderid-text-field__label, +.app.dark .thunderid-text-field__label, +.dark .thunderid-input-label, +html.dark .thunderid-input-label, +.app.dark .thunderid-input-label { + color: var(--color-text) !important; +} + +.dark .thunderid-password-field__input, +html.dark .thunderid-password-field__input, +.app.dark .thunderid-password-field__input { + background: transparent !important; + color: var(--color-text) !important; +} + +.dark .thunderid-password-field__input::placeholder, +html.dark .thunderid-password-field__input::placeholder, +.app.dark .thunderid-password-field__input::placeholder, +.dark .thunderid-text-field__input::placeholder, +html.dark .thunderid-text-field__input::placeholder, +.app.dark .thunderid-text-field__input::placeholder { + color: var(--color-text-muted) !important; +} + +.dark .thunderid-password-field__toggle, +html.dark .thunderid-password-field__toggle, +.app.dark .thunderid-password-field__toggle { + color: var(--color-text-muted) !important; + background: transparent !important; +} + +.dark .thunderid-password-field__toggle:hover, +html.dark .thunderid-password-field__toggle:hover, +.app.dark .thunderid-password-field__toggle:hover { + color: var(--color-text) !important; +} + +.dark .thunderid-change-credential__requirements-heading, +html.dark .thunderid-change-credential__requirements-heading, +.app.dark .thunderid-change-credential__requirements-heading, +.dark .thunderid-change-credential__requirement, +html.dark .thunderid-change-credential__requirement, +.app.dark .thunderid-change-credential__requirement { + color: var(--color-text-muted) !important; +} + +.dark .thunderid-change-credential__requirement--passed, +html.dark .thunderid-change-credential__requirement--passed, +.app.dark .thunderid-change-credential__requirement--passed { + color: #38c172 !important; +} + +/* ── ThunderID UserDropdown dark mode ── */ +.dark .thunderid-user-dropdown__menu, +html.dark .thunderid-user-dropdown__menu, +.app.dark .thunderid-user-dropdown__menu, +.dark .thunderid-user-dropdown__content, +html.dark .thunderid-user-dropdown__content, +.app.dark .thunderid-user-dropdown__content { + background-color: var(--color-card) !important; + border: 1px solid var(--color-border) !important; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45) !important; + color: var(--color-text) !important; +} + +.dark .thunderid-user-dropdown__menu-header, +html.dark .thunderid-user-dropdown__menu-header, +.app.dark .thunderid-user-dropdown__menu-header, +.dark .thunderid-user-dropdown__header, +html.dark .thunderid-user-dropdown__header, +.app.dark .thunderid-user-dropdown__header { + border-bottom: 1px solid var(--color-border) !important; +} + +.dark .thunderid-user-dropdown__menu-header-name, +html.dark .thunderid-user-dropdown__menu-header-name, +.app.dark .thunderid-user-dropdown__menu-header-name, +.dark .thunderid-user-dropdown__header-name, +html.dark .thunderid-user-dropdown__header-name, +.app.dark .thunderid-user-dropdown__header-name { + color: var(--color-text) !important; +} + +.dark .thunderid-user-dropdown__menu-header-subtitle, +html.dark .thunderid-user-dropdown__menu-header-subtitle, +.app.dark .thunderid-user-dropdown__menu-header-subtitle, +.dark .thunderid-user-dropdown__header-subtitle, +html.dark .thunderid-user-dropdown__header-subtitle, +.app.dark .thunderid-user-dropdown__header-subtitle { + color: var(--color-text-muted) !important; +} + +.dark .thunderid-user-dropdown__menu-divider, +html.dark .thunderid-user-dropdown__menu-divider, +.app.dark .thunderid-user-dropdown__menu-divider, +.dark .thunderid-divider, +html.dark .thunderid-divider, +.app.dark .thunderid-divider { + background-color: var(--color-border) !important; + border-color: var(--color-border) !important; +} + +.dark .thunderid-user-dropdown__item, +html.dark .thunderid-user-dropdown__item, +.app.dark .thunderid-user-dropdown__item { + color: var(--color-text) !important; +} + +.dark .thunderid-user-dropdown__item:hover, +html.dark .thunderid-user-dropdown__item:hover, +.app.dark .thunderid-user-dropdown__item:hover { + background-color: rgba(255, 255, 255, 0.06) !important; + color: var(--color-text) !important; +} + +.dark .thunderid-user-dropdown__item--danger, +html.dark .thunderid-user-dropdown__item--danger, +.app.dark .thunderid-user-dropdown__item--danger { + color: #f87171 !important; +} + +.dark .thunderid-user-dropdown__item--danger:hover, +html.dark .thunderid-user-dropdown__item--danger:hover, +.app.dark .thunderid-user-dropdown__item--danger:hover { + background-color: rgba(239, 68, 68, 0.12) !important; +} + +.dark .thunderid-card, +html.dark .thunderid-card, +.app.dark .thunderid-card { + background-color: var(--color-card) !important; + border-color: var(--color-border) !important; + color: var(--color-text) !important; +} + diff --git a/quickstarts/nextjs/app/layout.tsx b/quickstarts/nextjs/app/layout.tsx new file mode 100644 index 0000000..d67dade --- /dev/null +++ b/quickstarts/nextjs/app/layout.tsx @@ -0,0 +1,47 @@ +import type { Metadata } from 'next' +import { ThunderIDProvider } from '@thunderid/nextjs/server' +import ConfigNotice from './components/ConfigNotice' +import './globals.css' + +export const dynamic = 'force-dynamic' + +export const metadata: Metadata = { + title: 'ThunderID Next.js Quickstart', + description: 'ThunderID authentication with Next.js', +} + +// Redirect-based flow (NEXT_PUBLIC_THUNDERID_CLIENT_ID set) sends the user to +// ThunderID's hosted pages and needs a registered redirect URI. The default, +// native flow renders sign-in/sign-up inline via the app's own routes and +// needs an application ID instead β€” no redirect URI or CORS setup required. +const isRedirectFlow = Boolean(process.env.NEXT_PUBLIC_THUNDERID_CLIENT_ID) + +const REQUIRED_ENV_VARS = [ + 'NEXT_PUBLIC_THUNDERID_BASE_URL', + 'THUNDERID_SECRET', + ...(isRedirectFlow + ? ['NEXT_PUBLIC_THUNDERID_CLIENT_ID', 'THUNDERID_CLIENT_SECRET'] + : ['NEXT_PUBLIC_THUNDERID_APPLICATION_ID', 'NEXT_PUBLIC_THUNDERID_SIGN_IN_URL', 'NEXT_PUBLIC_THUNDERID_SIGN_UP_URL', 'THUNDERID_FLOW_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/quickstarts/nextjs/app/not-found.tsx b/quickstarts/nextjs/app/not-found.tsx new file mode 100644 index 0000000..53bd47d --- /dev/null +++ b/quickstarts/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/quickstarts/nextjs/app/page.tsx b/quickstarts/nextjs/app/page.tsx new file mode 100644 index 0000000..825ba9f --- /dev/null +++ b/quickstarts/nextjs/app/page.tsx @@ -0,0 +1,264 @@ +'use client' +import { SignedIn, SignedOut, User, UserAvatar, useThunderID } from '@thunderid/nextjs' +import { useState, useEffect } from 'react' +import { fetchAccessToken } from './actions' +import HeroCtas from './components/HeroCtas' +import Nav from './components/Nav' +import ThunderMark from './components/ThunderMark' + +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(unixSeconds: number | undefined): string { + if (!unixSeconds) return 'β€”' + return new Date(unixSeconds * 1000).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: 'Explore use cases', + body: 'See what you can build β€” auth flows for web, mobile, APIs, and agents.', + cta: 'Browse use cases', + href: 'https://thunderid.dev/docs/next/use-cases/overview/', + }, + { + n: '02', + title: 'Learn about flows', + body: 'Understand how authorization code, PKCE, client credentials, and device flows work.', + cta: 'Read guide', + href: 'https://thunderid.dev/docs/next/guides/flows/what-are-flows/', + }, + { + n: '03', + title: 'Style your experience', + body: 'Customize the login UI, branding, and email templates to match your product.', + cta: 'Design guide', + href: 'https://thunderid.dev/docs/next/guides/design/overview/', + }, + { + n: '04', + title: 'Explore SDK APIs', + body: 'Full Next.js SDK reference β€” hooks, components, and configuration options.', + cta: 'SDK reference', + href: 'https://thunderid.dev/docs/next/sdks/nextjs/overview/', + }, +] + +function ClockIcon() { + return ( + + ) +} + +function TimerIcon() { + return ( + + ) +} + +function BuildingIcon() { + return ( + + ) +} + +function ArrowIcon() { + return ( + + ) +} + +function HomeContent() { + const { organizationHandle } = useThunderID() + const [accessToken, setAccessToken] = useState(null) + const [now, setNow] = useState(Math.floor(Date.now() / 1000)) + + useEffect(() => { + void fetchAccessToken().then((t) => { if (t) setAccessToken(t) }) + }, []) + + useEffect(() => { + const id = setInterval(() => setNow(Math.floor(Date.now() / 1000)), 1000) + return () => clearInterval(id) + }, []) + + const jwtPayload = accessToken ? (() => { + try { + const part = accessToken.split('.')[1] ?? '' + const base64 = part.replace(/-/g, '+').replace(/_/g, '/') + const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, '=') + return JSON.parse(atob(padded)) as Record + } catch { return null } + })() : null + + const authTime = jwtPayload?.auth_time as number | undefined + const exp = jwtPayload?.exp as number | undefined + const secsLeft = exp ? Math.max(0, exp - now) : null + const countdown = secsLeft !== null ? formatCountdown(secsLeft) : null + + return ( + + {(user: { givenName?: string; given_name?: string; familyName?: string; email?: string } | null) => { + const givenName = user?.givenName ?? user?.given_name ?? user?.email?.split('@')[0] ?? 'there' + const email = user?.email + const ouName = organizationHandle ?? 'Default' + + return ( +
    +
    + +
    +

    {greeting(givenName)}

    +
    + {email && {email}} + {email && } + + + Session active + +
    +
    +
    + +
    +
    +
    +
    +
    {formatTime(authTime)}
    +
    Signed in at
    +
    +
    +
    +
    +
    +
    + {countdown ? countdown.text : 'β€”'} +
    +
    Session expires in
    +
    +
    +
    +
    +
    +
    {ouName}
    +
    Organisation
    +
    +
    +
    + +
    What's next
    + +
    + ) + }} +
    + ) +} + +export default function HomePage() { + return ( +
    +
    + ) +} diff --git a/quickstarts/nextjs/app/signin/page.tsx b/quickstarts/nextjs/app/signin/page.tsx new file mode 100644 index 0000000..26ab4ce --- /dev/null +++ b/quickstarts/nextjs/app/signin/page.tsx @@ -0,0 +1,30 @@ +'use client' +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import { SignedIn, SignedOut, SignIn } from '@thunderid/nextjs' +import Nav from '../components/Nav' + +export default function SignInPage() { + const router = useRouter() + + return ( +
    +
    + ) +} diff --git a/quickstarts/nextjs/app/signup/page.tsx b/quickstarts/nextjs/app/signup/page.tsx new file mode 100644 index 0000000..d185861 --- /dev/null +++ b/quickstarts/nextjs/app/signup/page.tsx @@ -0,0 +1,27 @@ +'use client' +import Link from 'next/link' +import { SignedIn, SignedOut, SignUp } from '@thunderid/nextjs' +import Nav from '../components/Nav' + +export default function SignUpPage() { + return ( +
    +
    + ) +} diff --git a/quickstarts/nextjs/app/token/page.tsx b/quickstarts/nextjs/app/token/page.tsx new file mode 100644 index 0000000..9d7f741 --- /dev/null +++ b/quickstarts/nextjs/app/token/page.tsx @@ -0,0 +1,143 @@ +'use client' +import { useState, useEffect } from 'react' +import { fetchAccessToken } from '../actions' +import Nav from '../components/Nav' + +function decodeJwtPart(part: string): Record | null { + try { + const base64 = part.replace(/-/g, '+').replace(/_/g, '/') + const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, '=') + return JSON.parse(atob(padded)) as Record + } catch { + return null + } +} + +function CopyIcon() { + return ( + + ) +} + +export default function TokenDebugPage() { + const [rawToken, setRawToken] = useState(null) + const [copied, setCopied] = useState(false) + const [now, setNow] = useState(Math.floor(Date.now() / 1000)) + + useEffect(() => { + void fetchAccessToken().then((t) => { if (t) setRawToken(t) }) + }, []) + + useEffect(() => { + const id = setInterval(() => setNow(Math.floor(Date.now() / 1000)), 1000) + return () => clearInterval(id) + }, []) + + const parts = rawToken ? rawToken.split('.') : [] + const jwtHeader = parts[0] ? decodeJwtPart(parts[0]) : null + const jwtPayload = parts[1] ? decodeJwtPart(parts[1]) : null + + const exp = jwtPayload?.exp as number | undefined + const expiresIn = exp ? exp - now : null + const expired = expiresIn !== null && expiresIn <= 0 + const minsLeft = expiresIn !== null ? Math.abs(Math.floor(expiresIn / 60)) : null + + const handleCopy = () => { + if (rawToken && navigator.clipboard) { + void navigator.clipboard.writeText(rawToken) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } + } + + const issuer = jwtPayload?.iss as string | undefined + const audience = Array.isArray(jwtPayload?.aud) ? (jwtPayload.aud as string[]).join(', ') : jwtPayload?.aud as string | undefined + const scopes = jwtPayload?.scope as string | undefined + + return ( +
    +
    + ) +} diff --git a/quickstarts/nextjs/next-env.d.ts b/quickstarts/nextjs/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/quickstarts/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/quickstarts/nextjs/next.config.ts b/quickstarts/nextjs/next.config.ts new file mode 100644 index 0000000..e4f5738 --- /dev/null +++ b/quickstarts/nextjs/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = {} + +export default nextConfig diff --git a/quickstarts/nextjs/package.json b/quickstarts/nextjs/package.json new file mode 100644 index 0000000..4c7613b --- /dev/null +++ b/quickstarts/nextjs/package.json @@ -0,0 +1,24 @@ +{ + "name": "@thunderid/nextjs-quickstart", + "private": true, + "version": "0.0.0", + "scripts": { + "prepare-dev": "node scripts/prepare-dev.cjs", + "prepare-dev:redirect": "node scripts/prepare-dev.cjs --flow=redirect", + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@thunderid/nextjs": "1.3.0", + "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/quickstarts/nextjs/pnpm-lock.yaml b/quickstarts/nextjs/pnpm-lock.yaml new file mode 100644 index 0000000..e2f0155 --- /dev/null +++ b/quickstarts/nextjs/pnpm-lock.yaml @@ -0,0 +1,1391 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + postcss: 8.5.23 + +importers: + + .: + dependencies: + '@thunderid/nextjs': + specifier: 1.3.0 + version: 1.3.0(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: + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.29.10': + resolution: {integrity: sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.10': + resolution: {integrity: sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/css@11.13.5': + resolution: {integrity: sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/react-dom@2.1.9': + resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.27.12': + resolution: {integrity: sha512-kKlWNrpIQxF1B/a2MZvE0/uyKby4960yjO91W7nVyNKmmfNi62xU9HCjL1M1eWzx/LFj/VPSwJVbwQk9Pq/68A==} + peerDependencies: + react: '>=17.0.0' + react-dom: '>=17.0.0' + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + + '@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] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@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] + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@thunderid/browser@1.3.0': + resolution: {integrity: sha512-O6DHlUyVFfBaNcJDEXACLU4NJMrNjDlqAuzesvJ+8a0Gl5g8WFK53rpx027e/rWICG0vcGe2UVZuhvZq5Z511A==} + + '@thunderid/javascript@1.4.0': + resolution: {integrity: sha512-qHfIFzXOQTN1nhuBT91Tr8bLPvfyEDa7gUQSL7GNcJkcyyKfJ77meIwr91lCme0ySFUnTeKl0Bk+R8iIJiKXBw==} + + '@thunderid/nextjs@1.3.0': + resolution: {integrity: sha512-MssmgDizw2OYy7p4+XJku3cHhz6TrTgjeyOGvc0TWD1f/en5/jggxy7g9RmDAuKPKRQ4C9eCAvtpHuvN73rMvg==} + peerDependencies: + next: '>=15.5.18' + react: '>=16.8.0' + + '@thunderid/node@1.3.0': + resolution: {integrity: sha512-hAFf4Bv3JZTcatF7Y/iarbd3lYLbIkz6SHzGKG5HOwh/a/Ip8qpu7u4npF6/0cNzrvZzY5g6awIymsIav75Xqw==} + + '@thunderid/react@1.4.0': + resolution: {integrity: sha512-MRSnxgAYCnQKXLaAcWd4oiX7H0RfMVtQr8eeDD8aA+TbJYYt2uSIuSEAgOad7iMbVL+xw174n/S/lw6ekKLBqg==} + peerDependencies: + '@types/react': '>=16.8.0' + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@types/node@24.19.2': + resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} + peerDependencies: + '@types/react': ^19.3.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001816: + resolution: {integrity: sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + core-js@3.42.0: + resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dom-walk@0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + + dompurify@3.4.13: + resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-random-values@3.0.0: + resolution: {integrity: sha512-mNznaBdYcpz7UAdnOtDGcLdNwAa79mXl5htEyyZ51YaeAWNf2g4x/2yCVBdNNTbi35wX0Stc2PJXM7G6rcONOA==} + engines: {node: 18 || >=20} + + global@4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-core-module@2.17.0: + resolution: {integrity: sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==} + engines: {node: '>= 0.4'} + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + memory-cache@0.2.0: + resolution: {integrity: sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==} + + min-document@2.19.2: + resolution: {integrity: sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + 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 + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + 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'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.13: + resolution: {integrity: sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==} + engines: {node: '>= 0.4'} + hasBin: true + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + secure-random-bytes@5.0.1: + resolution: {integrity: sha512-48nvkahmdyoRbHW/x1HTGIk+i2/84MHFBB+rZ50cg1y/p2RjKG+fhTMQKZx4QD6T6/o2M2WHLPkncqQpjMMsUw==} + engines: {node: 18 || >=20} + + secure-random-octet@4.0.1: + resolution: {integrity: sha512-KEeg9wGi/96Q8Edp97gt5Z5dYCcvG7vPKdKFq6rPLCHtaaHwZKbhbC+gY74O7OjX8Fnels49wqc8IgkNhBJZfw==} + engines: {node: 18 || >=20} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + 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'} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + stream-browserify@3.0.0: + resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + 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 + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tabbable@6.5.0: + resolution: {integrity: sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + 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==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + + '@babel/runtime@7.29.10': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.10': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.29.7 + '@babel/runtime': 7.29.10 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/css@11.13.5': + dependencies: + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + transitivePeerDependencies: + - supports-color + + '@emotion/hash@0.9.2': {} + + '@emotion/memoize@0.9.0': {} + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.2.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/unitless@0.10.0': {} + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/react-dom@2.1.9(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@floating-ui/dom': 1.8.0 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + + '@floating-ui/react@0.27.12(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.9(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@floating-ui/utils': 0.2.12 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + tabbable: 6.5.0 + + '@floating-ui/utils@0.2.12': {} + + '@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 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@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 + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@thunderid/browser@1.3.0': + dependencies: + '@thunderid/javascript': 1.4.0 + base64url: 3.0.1 + buffer: 6.0.3 + core-js: 3.42.0 + fast-sha256: 1.3.0 + jose: 6.2.3 + process: 0.11.10 + randombytes: 2.1.0 + stream-browserify: 3.0.0 + tslib: 2.8.1 + + '@thunderid/javascript@1.4.0': + dependencies: + jose: 6.2.3 + tslib: 2.8.1 + + '@thunderid/nextjs@1.3.0(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: + '@thunderid/node': 1.3.0 + '@thunderid/react': 1.4.0(@types/react@19.2.14)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@types/react': 19.2.14 + jose: 6.2.3 + 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 + tslib: 2.8.1 + transitivePeerDependencies: + - encoding + - react-dom + - supports-color + + '@thunderid/node@1.3.0': + dependencies: + '@thunderid/javascript': 1.4.0 + base64url: 3.0.1 + cross-fetch: 4.1.0 + fast-sha256: 1.3.0 + jose: 6.2.3 + memory-cache: 0.2.0 + secure-random-bytes: 5.0.1 + tslib: 2.8.1 + uuid: 11.1.1 + transitivePeerDependencies: + - encoding + + '@thunderid/react@1.4.0(@types/react@19.2.14)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@emotion/css': 11.13.5 + '@floating-ui/react': 0.27.12(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@thunderid/browser': 1.3.0 + '@types/react': 19.2.14 + dompurify: 3.4.13 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@types/node@24.19.2': + dependencies: + undici-types: 7.24.6 + + '@types/parse-json@4.0.2': {} + + '@types/react-dom@19.3.0(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + '@types/trusted-types@2.0.7': + optional: true + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.29.10 + cosmiconfig: 7.1.0 + resolve: 1.22.13 + + base64-js@1.5.1: {} + + base64url@3.0.1: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001816: {} + + client-only@0.0.1: {} + + convert-source-map@1.9.0: {} + + core-js@3.42.0: {} + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.3 + + cross-fetch@4.1.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-libc@2.1.2: + optional: true + + dom-walk@0.1.2: {} + + dompurify@3.4.13: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-errors@1.3.0: {} + + escape-string-regexp@4.0.0: {} + + fast-sha256@1.3.0: {} + + find-root@1.1.0: {} + + function-bind@1.1.2: {} + + get-random-values@3.0.0: + dependencies: + global: 4.4.0 + + global@4.4.0: + dependencies: + min-document: 2.19.2 + process: 0.11.10 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + ieee754@1.2.1: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + inherits@2.0.4: {} + + is-arrayish@0.2.1: {} + + is-core-module@2.17.0: + dependencies: + hasown: 2.0.4 + + jose@6.2.3: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + lines-and-columns@1.2.4: {} + + memory-cache@0.2.0: {} + + min-document@2.19.2: + dependencies: + dom-walk: 0.1.2 + + ms@2.1.3: {} + + nanoid@3.3.20: {} + + 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.30001816 + 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 + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.7 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + postcss@8.5.23: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + process@0.11.10: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + react-dom@19.3.1(react@19.3.1): + dependencies: + react: 19.3.1 + scheduler: 0.28.0 + + react@19.3.1: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + resolve-from@4.0.0: {} + + resolve@1.22.13: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.17.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + safe-buffer@5.2.1: {} + + scheduler@0.28.0: {} + + secure-random-bytes@5.0.1: + dependencies: + secure-random-octet: 4.0.1 + + secure-random-octet@4.0.1: + dependencies: + get-random-values: 3.0.0 + + semver@7.8.5: + optional: true + + 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: {} + + source-map@0.5.7: {} + + stream-browserify@3.0.0: + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + styled-jsx@5.1.6(react@19.3.1): + dependencies: + client-only: 0.0.1 + react: 19.3.1 + + stylis@4.2.0: {} + + supports-preserve-symlinks-flag@1.0.0: {} + + tabbable@6.5.0: {} + + tr46@0.0.3: {} + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + undici-types@7.24.6: {} + + util-deprecate@1.0.2: {} + + uuid@11.1.1: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + yaml@1.10.3: {} diff --git a/quickstarts/nextjs/pnpm-workspace.yaml b/quickstarts/nextjs/pnpm-workspace.yaml new file mode 100644 index 0000000..f8d813f --- /dev/null +++ b/quickstarts/nextjs/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +allowBuilds: + core-js: false + +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 `@thunderid/nextjs` > `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/quickstarts/nextjs/proxy.ts b/quickstarts/nextjs/proxy.ts new file mode 100644 index 0000000..bada0be --- /dev/null +++ b/quickstarts/nextjs/proxy.ts @@ -0,0 +1,13 @@ +import {thunderIDProxy, createRouteMatcher} from '@thunderid/nextjs/server' + +const isProtectedRoute = createRouteMatcher(['/dashboard(.*)']) + +export default thunderIDProxy(async (thunderid, request) => { + if (isProtectedRoute(request)) { + await thunderid.protectRoute() + } +}) + +export const config = { + matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'], +} diff --git a/quickstarts/nextjs/scripts/prepare-dev.cjs b/quickstarts/nextjs/scripts/prepare-dev.cjs new file mode 100644 index 0000000..1e74cc2 --- /dev/null +++ b/quickstarts/nextjs/scripts/prepare-dev.cjs @@ -0,0 +1,78 @@ +// 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 PREFIX = 'NEXT_PUBLIC_THUNDERID_'; +const NATIVE_FLOW_VARS = [ + `${PREFIX}APPLICATION_ID`, + `${PREFIX}SIGN_IN_URL`, + `${PREFIX}SIGN_UP_URL`, + 'THUNDERID_FLOW_SECRET', +]; +const REDIRECT_FLOW_VARS = [`${PREFIX}CLIENT_ID`, 'THUNDERID_CLIENT_SECRET']; +const REDIRECT_FLOW_PLACEHOLDERS = { + [`${PREFIX}CLIENT_ID`]: 'your-client-id-here', + THUNDERID_CLIENT_SECRET: 'your-client-secret-here', +}; + +const flowArg = process.argv.find((arg) => arg.startsWith('--flow=')); +const flowExplicitlyRequested = Boolean(flowArg); +const flow = flowArg ? flowArg.slice('--flow='.length) : 'native'; + +if (flowExplicitlyRequested && flow !== 'native' && flow !== 'redirect') { + console.error(`Unknown --flow value "${flow}". Expected "native" or "redirect".`); + process.exit(1); +} + +/** + * Toggles the leading `# ` on env var lines to match the selected flow, appending + * any vars for the selected flow that aren't present in the source file yet (the + * redirect-flow vars aren't checked into `.env.example`, so switching to `redirect` + * from a fresh copy needs to add them rather than just uncomment them). + */ +function applyFlow(envContent, selectedFlow) { + const varsToEnable = selectedFlow === 'redirect' ? REDIRECT_FLOW_VARS : NATIVE_FLOW_VARS; + const varsToDisable = selectedFlow === 'redirect' ? NATIVE_FLOW_VARS : REDIRECT_FLOW_VARS; + const found = new Set(); + + const lines = envContent.split('\n').map((line) => { + const enable = varsToEnable.find((key) => line.replace(/^#\s*/, '').startsWith(`${key}=`)); + if (enable) { + found.add(enable); + return line.replace(/^#\s*/, ''); + } + + const disable = varsToDisable.find((key) => line.startsWith(`${key}=`)); + if (disable) return `# ${line}`; + + return line; + }); + + const missing = varsToEnable.filter((key) => !found.has(key)); + if (missing.length > 0) { + lines.push('', `# ── ${selectedFlow} flow ─────────────────────────────────────────────────`); + for (const key of missing) lines.push(`${key}=${REDIRECT_FLOW_PLACEHOLDERS[key] ?? ''}`); + } + + return lines.join('\n'); +} + +const envExample = path.join(root, '.env.example'); +const envTarget = path.join(root, '.env'); + +// Only explicit `--flow=` invocations are allowed to (re)write an existing +// .env β€” a plain `npm run prepare-dev` (e.g. from .stackblitzrc's +// startCommand) stays a safe, idempotent copy-if-missing. +if (fs.existsSync(envExample) && (flowExplicitlyRequested || !fs.existsSync(envTarget))) { + const envSource = fs.existsSync(envTarget) ? envTarget : envExample; + const envContent = applyFlow(fs.readFileSync(envSource, 'utf8'), flow) + // 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/quickstarts/nextjs/tsconfig.json b/quickstarts/nextjs/tsconfig.json new file mode 100644 index 0000000..fba2bf3 --- /dev/null +++ b/quickstarts/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": { "@/*": ["./src/*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/quickstarts/node/.env.example b/quickstarts/node/.env.example new file mode 100644 index 0000000..a45eda1 --- /dev/null +++ b/quickstarts/node/.env.example @@ -0,0 +1,14 @@ +# Base URL of your ThunderID deployment. ex: https://localhost:8090 +THUNDERID_BASE_URL=https://localhost:8090 + +# OAuth2 Client ID for this service. ThunderID Console -> your application -> Overview. +THUNDERID_CLIENT_ID=your-client-id +# OAuth2 Client Secret for this service. Shown once when the application is created; +# if lost, regenerate it from the app's Credentials tab (under Edit). +THUNDERID_CLIENT_SECRET=your-client-secret + +# Optional: space-separated scopes to request for this service. +THUNDERID_SCOPE= + +# 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/quickstarts/node/.gitignore b/quickstarts/node/.gitignore new file mode 100644 index 0000000..37d7e73 --- /dev/null +++ b/quickstarts/node/.gitignore @@ -0,0 +1,2 @@ +node_modules +.env diff --git a/quickstarts/node/.stackblitzrc b/quickstarts/node/.stackblitzrc new file mode 100644 index 0000000..5f0a200 --- /dev/null +++ b/quickstarts/node/.stackblitzrc @@ -0,0 +1,4 @@ +{ + "installDependencies": false, + "startCommand": "npm run prepare-dev && npm install && npm run dev" +} diff --git a/quickstarts/node/README.md b/quickstarts/node/README.md new file mode 100644 index 0000000..c833add --- /dev/null +++ b/quickstarts/node/README.md @@ -0,0 +1,79 @@ +# ThunderID Node.js Service Quickstart + +Open in StackBlitz + +A minimal machine-to-machine (service-to-service) client, using the ThunderID Node.js SDK +(`@thunderid/node`). Unlike the other quickstarts, there's no user and no browser sign-in: this +service authenticates as **itself** with the OAuth 2.0 `client_credentials` grant, then uses the +resulting access token to call another piece of business logic. + +This is the pattern to use for background jobs, cron tasks, or one backend service calling +another, where there's no human in the loop to redirect through a login page. + +The sample is organized the way a real backend usually is, with authentication kept in its own +service instead of scattered across business logic: + +```text +quickstarts/node/ +β”œβ”€β”€ index.mjs Wires the services together and prints the result +└── lib/ + β”œβ”€β”€ AuthService.mjs Acquires and caches this service's own access token + β”œβ”€β”€ InventoryService.mjs Business logic; asks AuthService for a token before answering + └── ui.mjs Terminal output, built on boxen and picocolors +``` + +## How it works + +- `lib/AuthService.mjs` initializes `ThunderIDNodeClient` with `grantType: 'client_credentials'`. + With that set, `getAccessToken()` authenticates as the service itself (no session, no sign-in) + and transparently fetches, caches, and refreshes the token, the same method a user-facing + quickstart would call to read an existing session's token, just authenticating as the service + itself instead of a signed-in user. +- `lib/InventoryService.mjs` is the business logic that needs a token before it can act. It asks + `AuthService` for one; the rest of the app never handles a token or an `Authorization` header + directly. In a real deployment, `InventoryService` would send that token to a separate inventory + backend over HTTP, in place of another service calling `AuthService`'s owner the same way. + +## Prerequisites + +- Node.js 18+ +- A running ThunderID instance (default: `https://localhost:8090`) +- A **Backend Service** application registered in ThunderID + +## Create an application + +1. Open the ThunderID Console (`https://localhost:8090/console`), navigate to **Applications**, and + click **Add Application**. +2. From the **Choose a type** page, select **Backend Service**. +3. Enter a name (e.g. `node-service-quickstart`) and create the application. Backend Service apps + are pre-configured for the `client_credentials` grant with `client_secret_basic` authentication, + no extra setup needed. +4. Copy the **Client ID** from the **General** tab, and the **Client Secret** from the window that + pops up when the application is created (shown only once), you'll need them below. + +## Getting started + +1. Copy the environment file and fill in your application's credentials: + ```sh + cp .env.example .env + ``` + ``` + THUNDERID_CLIENT_ID= + THUNDERID_CLIENT_SECRET= + THUNDERID_BASE_URL=https://localhost:8090 + ``` + +2. Install dependencies and run it: + ```sh + npm install + npm start + ``` + +The script authenticates once, then calls `InventoryService.getStock()` for a few SKUs and prints +the result before exiting: a scope line, then an `Inventory` panel with one line per SKU, green +for in-stock items, yellow for out-of-stock, red for a SKU that doesn't exist. + +## Learn more + +- [ThunderID Docs](https://thunderid.dev/docs) +- [OAuth 2.0 Client Credentials Grant (RFC 6749 Β§4.4)](https://www.rfc-editor.org/rfc/rfc6749#section-4.4) diff --git a/quickstarts/node/index.mjs b/quickstarts/node/index.mjs new file mode 100644 index 0000000..8d01c87 --- /dev/null +++ b/quickstarts/node/index.mjs @@ -0,0 +1,42 @@ +import {AuthService} from './lib/AuthService.mjs'; +import {InventoryService} from './lib/InventoryService.mjs'; +import {printIntro, printConfigNeeded, printAuthenticated, printInventory} from './lib/ui.mjs'; + +printIntro(); + +const REQUIRED_ENV_VARS = ['THUNDERID_CLIENT_ID', 'THUNDERID_CLIENT_SECRET']; +const missingEnvVars = REQUIRED_ENV_VARS.filter((key) => !process.env[key]); + +if (missingEnvVars.length > 0) { + printConfigNeeded(missingEnvVars); + process.exit(1); +} + +const THUNDER_BASE_URL = process.env.THUNDERID_BASE_URL || 'https://localhost:8090'; + +// AuthService authenticates as this service itself (grantType: 'client_credentials'), +// no user and no browser sign-in, unlike the express/react/etc quickstarts, which +// authenticate a human through a login page. +const auth = new AuthService({ + baseUrl: THUNDER_BASE_URL, + clientId: process.env.THUNDERID_CLIENT_ID, + clientSecret: process.env.THUNDERID_CLIENT_SECRET, + scopes: process.env.THUNDERID_SCOPE, +}); + +const inventory = new InventoryService({auth}); + +const accessToken = await auth.getAccessToken(); +const {scope} = await auth.decodeAccessToken(accessToken); +printAuthenticated({baseUrl: THUNDER_BASE_URL, clientId: process.env.THUNDERID_CLIENT_ID, scope}); + +const rows = []; +for (const sku of ['SKU-100', 'SKU-200', 'SKU-300', 'SKU-999']) { + try { + const item = await inventory.getStock(sku); + rows.push({sku, name: item.name, stock: item.stock}); + } catch (error) { + rows.push({sku, error: error.message}); + } +} +printInventory(rows); diff --git a/quickstarts/node/lib/AuthService.mjs b/quickstarts/node/lib/AuthService.mjs new file mode 100644 index 0000000..e827c15 --- /dev/null +++ b/quickstarts/node/lib/AuthService.mjs @@ -0,0 +1,33 @@ +import {ThunderIDNodeClient} from '@thunderid/node'; + +/** + * Authenticates this service as itself with the OAuth 2.0 client_credentials + * grant, no user, no browser sign-in. Other services in this process (or a + * real deployment) ask AuthService for a token instead of talking to + * ThunderIDNodeClient directly, so the authentication concern stays in one + * place and out of business logic like InventoryService. + */ +export class AuthService { + #client = new ThunderIDNodeClient(); + #ready; + + constructor({baseUrl, clientId, clientSecret, scopes}) { + this.#ready = this.#client.initialize({ + baseUrl, + clientId, + clientSecret, + grantType: 'client_credentials', + scopes, + }); + } + + /** Fetches (or reuses the cached) access token for this service's own identity. */ + async getAccessToken() { + await this.#ready; + return this.#client.getAccessToken(); + } + + async decodeAccessToken(accessToken) { + return this.#client.decodeJwtToken(accessToken); + } +} diff --git a/quickstarts/node/lib/InventoryService.mjs b/quickstarts/node/lib/InventoryService.mjs new file mode 100644 index 0000000..9897c79 --- /dev/null +++ b/quickstarts/node/lib/InventoryService.mjs @@ -0,0 +1,29 @@ +const CATALOG = { + 'SKU-100': {sku: 'SKU-100', name: 'Wireless Mouse', stock: 42}, + 'SKU-200': {sku: 'SKU-200', name: 'Mechanical Keyboard', stock: 7}, + 'SKU-300': {sku: 'SKU-300', name: '4K Monitor', stock: 0}, +}; + +/** + * Business logic for a service that needs to call another backend. It asks + * AuthService for a token before answering, so the rest of the app never + * handles a token or an Authorization header directly. In a real deployment + * this would send the token to a separate inventory service over HTTP + * instead of just holding it in-process. + */ +export class InventoryService { + constructor({auth}) { + this.auth = auth; + } + + async getStock(sku) { + // A real backend would attach this as `Authorization: Bearer ${accessToken}` + // on the request to the inventory service; here it just proves the token + // was obtained before answering. + await this.auth.getAccessToken(); + + const item = CATALOG[sku]; + if (!item) throw new Error(`No item with SKU ${sku}`); + return item; + } +} diff --git a/quickstarts/node/lib/ui.mjs b/quickstarts/node/lib/ui.mjs new file mode 100644 index 0000000..f234454 --- /dev/null +++ b/quickstarts/node/lib/ui.mjs @@ -0,0 +1,89 @@ +import boxen from 'boxen'; +import pc from 'picocolors'; + +const BRAND = '#3688FF'; +const ORANGE = '#e88b3a'; + +function indent(text) { + return text + .split('\n') + .map((line) => ` ${line}`) + .join('\n'); +} + +function badge(label, colorFn) { + const line = pc.dim('──────'); + return `${line} ${pc.bold(colorFn(label.toUpperCase()))} ${line}`; +} + +function box(content, {title, borderColor}) { + return boxen(content, { + title, + titleAlignment: 'left', + padding: {top: 0, bottom: 0, left: 1, right: 1}, + borderStyle: 'round', + borderColor, + }); +} + +export function printIntro() { + console.log(); + console.log(indent(pc.bold('ThunderID Node Service Quickstart'))); + console.log(indent(pc.dim('Service-to-service auth via OAuth 2.0 client_credentials'))); + console.log(); +} + +export function printConfigNeeded(missingEnvVars) { + console.log(indent(badge('Setup required', pc.yellow))); + console.log(); + console.log(indent(pc.bold('Configuration needed'))); + console.log( + indent( + pc.dim( + "This quickstart can't reach ThunderID yet. Set the following environment variable(s), then run again.", + ), + ), + ); + console.log(); + console.log(indent(box(missingEnvVars.map((key) => pc.red(key)).join('\n'), {borderColor: ORANGE}))); + console.log(); + console.log( + indent( + pc.dim('Copy ') + + pc.cyan('.env.example') + + pc.dim(' to ') + + pc.cyan('.env') + + pc.dim(', fill in the client credentials for your application, then run ') + + pc.cyan('npm start') + + pc.dim(' again.'), + ), + ); + console.log(); + console.log( + indent( + pc.dim('Need more info? Take a look at the Node quickstart guide: ') + + pc.cyan('https://thunderid.dev/docs/next/getting-started/connect-your-application/node/'), + ), + ); + console.log(); +} + +export function printAuthenticated({baseUrl, clientId, scope}) { + console.log(indent(`${pc.bgGreen(pc.black(' ● AUTHENTICATED '))} ${pc.dim('client_credentials grant')}`)); + console.log(); + console.log(indent(`${pc.dim('BASE URL')} ${baseUrl}`)); + console.log(indent(`${pc.dim('CLIENT ID')} ${clientId}`)); + console.log(indent(`${pc.dim('SCOPE')} ${scope || pc.dim('(none requested)')}`)); + console.log(); +} + +export function printInventory(rows) { + const lines = rows.map(({sku, name, stock, error}) => { + const label = pc.bold(sku.padEnd(9)); + if (error) return `${label} ${pc.red(error)}`; + const stockText = stock > 0 ? pc.green(`${stock} in stock`) : pc.yellow('out of stock'); + return `${label} ${name.padEnd(20)} ${stockText}`; + }); + console.log(indent(box(lines.join('\n'), {title: 'Inventory', borderColor: BRAND}))); + console.log(); +} diff --git a/quickstarts/node/package.json b/quickstarts/node/package.json new file mode 100644 index 0000000..0b6ced2 --- /dev/null +++ b/quickstarts/node/package.json @@ -0,0 +1,16 @@ +{ + "name": "@thunderid/node-quickstart", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "prepare-dev": "node scripts/prepare-dev.cjs", + "start": "node --env-file-if-exists=.env index.mjs", + "dev": "node --env-file-if-exists=.env --watch index.mjs" + }, + "dependencies": { + "@thunderid/node": "1.3.0", + "boxen": "^8.0.1", + "picocolors": "^1.1.1" + } +} diff --git a/quickstarts/node/pnpm-lock.yaml b/quickstarts/node/pnpm-lock.yaml new file mode 100644 index 0000000..b25f74b --- /dev/null +++ b/quickstarts/node/pnpm-lock.yaml @@ -0,0 +1,315 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@thunderid/node': + specifier: 1.3.0 + version: 1.3.0 + boxen: + specifier: ^8.0.1 + version: 8.0.1 + picocolors: + specifier: ^1.1.1 + version: 1.1.1 + +packages: + + '@thunderid/javascript@1.4.0': + resolution: {integrity: sha512-qHfIFzXOQTN1nhuBT91Tr8bLPvfyEDa7gUQSL7GNcJkcyyKfJ77meIwr91lCme0ySFUnTeKl0Bk+R8iIJiKXBw==} + + '@thunderid/node@1.3.0': + resolution: {integrity: sha512-hAFf4Bv3JZTcatF7Y/iarbd3lYLbIkz6SHzGKG5HOwh/a/Ip8qpu7u4npF6/0cNzrvZzY5g6awIymsIav75Xqw==} + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.4.0: + resolution: {integrity: sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==} + engines: {node: '>=12'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + + boxen@8.0.1: + resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} + engines: {node: '>=18'} + + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} + + dom-walk@0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + get-east-asian-width@1.7.0: + resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==} + engines: {node: '>=18'} + + get-random-values@3.0.0: + resolution: {integrity: sha512-mNznaBdYcpz7UAdnOtDGcLdNwAa79mXl5htEyyZ51YaeAWNf2g4x/2yCVBdNNTbi35wX0Stc2PJXM7G6rcONOA==} + engines: {node: 18 || >=20} + + global@4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + memory-cache@0.2.0: + resolution: {integrity: sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==} + + min-document@2.19.2: + resolution: {integrity: sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + secure-random-bytes@5.0.1: + resolution: {integrity: sha512-48nvkahmdyoRbHW/x1HTGIk+i2/84MHFBB+rZ50cg1y/p2RjKG+fhTMQKZx4QD6T6/o2M2WHLPkncqQpjMMsUw==} + engines: {node: 18 || >=20} + + secure-random-octet@4.0.1: + resolution: {integrity: sha512-KEeg9wGi/96Q8Edp97gt5Z5dYCcvG7vPKdKFq6rPLCHtaaHwZKbhbC+gY74O7OjX8Fnels49wqc8IgkNhBJZfw==} + engines: {node: 18 || >=20} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + widest-line@5.0.0: + resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} + engines: {node: '>=18'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + +snapshots: + + '@thunderid/javascript@1.4.0': + dependencies: + jose: 6.2.3 + tslib: 2.8.1 + + '@thunderid/node@1.3.0': + dependencies: + '@thunderid/javascript': 1.4.0 + base64url: 3.0.1 + cross-fetch: 4.1.0 + fast-sha256: 1.3.0 + jose: 6.2.3 + memory-cache: 0.2.0 + secure-random-bytes: 5.0.1 + tslib: 2.8.1 + uuid: 11.1.1 + transitivePeerDependencies: + - encoding + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.4.0: {} + + ansi-styles@6.2.3: {} + + base64url@3.0.1: {} + + boxen@8.0.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 8.0.0 + chalk: 5.6.2 + cli-boxes: 3.0.0 + string-width: 7.2.0 + type-fest: 4.41.0 + widest-line: 5.0.0 + wrap-ansi: 9.0.2 + + camelcase@8.0.0: {} + + chalk@5.6.2: {} + + cli-boxes@3.0.0: {} + + cross-fetch@4.1.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + dom-walk@0.1.2: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + fast-sha256@1.3.0: {} + + get-east-asian-width@1.7.0: {} + + get-random-values@3.0.0: + dependencies: + global: 4.4.0 + + global@4.4.0: + dependencies: + min-document: 2.19.2 + process: 0.11.10 + + is-fullwidth-code-point@3.0.0: {} + + jose@6.2.3: {} + + memory-cache@0.2.0: {} + + min-document@2.19.2: + dependencies: + dom-walk: 0.1.2 + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + picocolors@1.1.1: {} + + process@0.11.10: {} + + secure-random-bytes@5.0.1: + dependencies: + secure-random-octet: 4.0.1 + + secure-random-octet@4.0.1: + dependencies: + get-random-values: 3.0.0 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.7.0 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.4.0 + + tr46@0.0.3: {} + + tslib@2.8.1: {} + + type-fest@4.41.0: {} + + uuid@11.1.1: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + widest-line@5.0.0: + dependencies: + string-width: 7.2.0 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 diff --git a/quickstarts/node/scripts/prepare-dev.cjs b/quickstarts/node/scripts/prepare-dev.cjs new file mode 100644 index 0000000..734e095 --- /dev/null +++ b/quickstarts/node/scripts/prepare-dev.cjs @@ -0,0 +1,19 @@ +// 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'); +if (fs.existsSync(envExample) && !fs.existsSync(envTarget)) { + // 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. + const envContent = fs + .readFileSync(envExample, 'utf8') + .replace(/^([A-Z0-9_]+)=(your-\S*|generate-with-\S*|https:\/\/localhost:8090)$/gm, '$1='); + fs.writeFileSync(envTarget, envContent); +} diff --git a/quickstarts/nuxt/.env.example b/quickstarts/nuxt/.env.example new file mode 100644 index 0000000..fbeed8e --- /dev/null +++ b/quickstarts/nuxt/.env.example @@ -0,0 +1,20 @@ +# Base URL of your ThunderID deployment. ex: https://localhost:8090 +NUXT_PUBLIC_THUNDERID_BASE_URL=https://localhost:8090 + +# Flow Secret for this app. Sent in the `Flow-Secret` header to authenticate this app when the +# native (embedded) flow starts. Shown once when the application is created; if lost, regenerate it +# from the app's Credentials tab (under Edit). Server-only, never expose to the browser. +THUNDERID_FLOW_SECRET=your-flow-secret-here + +# Secret used to encrypt this app's session cookie. Generate locally, not from the console. +THUNDERID_SESSION_SECRET=generate-with-openssl-rand-base64-32 + +# Application ID (spId) for this app. ThunderID Console -> your application -> Overview. +NUXT_PUBLIC_THUNDERID_APPLICATION_ID=your-application-id-here +# Local app route that renders the sign-in page. Not from the console. +NUXT_PUBLIC_THUNDERID_SIGN_IN_URL=/signin +# Local app route that renders the sign-up page. Not from the console. +NUXT_PUBLIC_THUNDERID_SIGN_UP_URL=/signup + +# 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/quickstarts/nuxt/.gitignore b/quickstarts/nuxt/.gitignore new file mode 100644 index 0000000..227706d --- /dev/null +++ b/quickstarts/nuxt/.gitignore @@ -0,0 +1,5 @@ +node_modules +.nuxt +.output +.data +.env diff --git a/quickstarts/nuxt/.stackblitzrc b/quickstarts/nuxt/.stackblitzrc new file mode 100644 index 0000000..5f0a200 --- /dev/null +++ b/quickstarts/nuxt/.stackblitzrc @@ -0,0 +1,4 @@ +{ + "installDependencies": false, + "startCommand": "npm run prepare-dev && npm install && npm run dev" +} diff --git a/quickstarts/nuxt/README.md b/quickstarts/nuxt/README.md new file mode 100644 index 0000000..b07ef57 --- /dev/null +++ b/quickstarts/nuxt/README.md @@ -0,0 +1,119 @@ +# ThunderID Nuxt Quickstart + +Open in StackBlitz + +A minimal Nuxt 3 application demonstrating ThunderID authentication with OAuth 2.0, PKCE, and JWT out of the box. + +## Prerequisites + +- Node.js 18+ +- pnpm +- A ThunderID application + +## Getting started + +1. Copy the example environment file: + + ```bash + cp .env.example .env + ``` + +2. Fill in your ThunderID credentials in `.env`. By default the file is set up for the native flow: + + ```dotenv + NUXT_PUBLIC_THUNDERID_BASE_URL=https://localhost:8090 + NUXT_PUBLIC_THUNDERID_APPLICATION_ID= + NUXT_PUBLIC_THUNDERID_SIGN_IN_URL=/signin + NUXT_PUBLIC_THUNDERID_SIGN_UP_URL=/signup + THUNDERID_FLOW_SECRET= + THUNDERID_SESSION_SECRET= + ``` + +3. Start the development server: + + ```bash + pnpm dev + ``` + + The app is now running at [http://localhost:3000](http://localhost:3000). + +
    +

    Redirect-based flow

    + +By default this quickstart uses the native (embedded) flow, where sign-in/sign-up render inline on this +app's own `/signin` and `/signup` routes with no redirect to ThunderID's hosted pages. + +To send the user to ThunderID's hosted sign-in page instead, switch to the redirect-based flow: + +1. Register a redirect URI for your application (see the app's config notice in the console for the + exact value to use). +2. Regenerate `.env` for the redirect flow: + + ```bash + npm run prepare-dev:redirect + ``` + + This comments out the native-flow vars (`NUXT_PUBLIC_THUNDERID_APPLICATION_ID`, + `NUXT_PUBLIC_THUNDERID_SIGN_IN_URL`, `NUXT_PUBLIC_THUNDERID_SIGN_UP_URL`) and adds: + + ```dotenv + NUXT_PUBLIC_THUNDERID_CLIENT_ID= + THUNDERID_CLIENT_SECRET= + ``` + + Both values come from the application's Credentials tab in the console. The redirect-based flow + doesn't use `THUNDERID_FLOW_SECRET` (that's only sent when the native flow starts), so it can stay + set in `.env` from step 2 above, unused. To switch back to the native flow, run + `node scripts/prepare-dev.cjs --flow=native` (or manually re-enable the native-flow vars and comment + out the two above). +3. Fill in `NUXT_PUBLIC_THUNDERID_CLIENT_ID` and `THUNDERID_CLIENT_SECRET` in `.env`, then restart the + dev server. + +
    + +## Back-channel logout + +ThunderID can end this app's session itself. When the user's ThunderID session ends somewhere else, +for example by signing out of another application, ThunderID posts a logout token to this app. The +module serves the route once it is turned on in `nuxt.config.ts`: + +```ts +thunderid: { + backchannelLogout: { enabled: true }, +}, +``` + +The session lives in a cookie that only the browser can delete, so the SDK records the logout and +treats every later request with that session as signed out. + +To try it, use the redirect-based flow above, since the logout token is addressed to the client ID: + +1. In the ThunderID Console, open this application, go to **Advanced Settings β†’ OAuth2 + Configuration**, and set **Back-Channel Logout URI** to + `http://localhost:3000/api/auth/backchannel-logout`. +2. ThunderID refuses localhost and private network addresses by default. For local testing only, + turn that off in the server's `deployment.yaml` and restart it: + ```yaml + oauth: + logout: + backchannel: + reject_private_addresses: false + ``` +3. Sign in at [http://localhost:3000](http://localhost:3000), then sign in to a second application + in the same browser, so both share one ThunderID session. +4. Sign out of the second application, then reload this app: you are signed out here too. + +Things to know before production: + +- The SDK records ended sessions in memory by default. If you run more than one instance, name a + [Nitro storage](https://nitro.build/guide/storage) mount point that all of them share, since + ThunderID's request reaches only one instance: + ```ts + thunderid: { + backchannelLogout: { enabled: true, store: 'redis' }, + }, + nitro: { + storage: { redis: { driver: 'redis', url: process.env.REDIS_URL } }, + }, + ``` +- An access token already handed to the browser or to another API stays valid until it expires. diff --git a/quickstarts/nuxt/app/app.vue b/quickstarts/nuxt/app/app.vue new file mode 100644 index 0000000..d463ae9 --- /dev/null +++ b/quickstarts/nuxt/app/app.vue @@ -0,0 +1,28 @@ + + + diff --git a/quickstarts/nuxt/app/assets/styles.css b/quickstarts/nuxt/app/assets/styles.css new file mode 100644 index 0000000..82719ff --- /dev/null +++ b/quickstarts/nuxt/app/assets/styles.css @@ -0,0 +1,1397 @@ +*, +::before, +::after { + box-sizing: border-box; +} + +/* ── Design tokens ── */ +.app { + --color-bg: #f7f9fc; + --color-card: #ffffff; + --color-border: #dde3ec; + --color-text: #05213f; + --color-text-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; + + font-family: var(--font-sans); + background: var(--color-bg); + color: var(--color-text); + min-height: 100vh; + display: flex; + flex-direction: column; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + transition: background 0.2s, color 0.2s; +} + +/* ── Dark mode ── */ +.app.dark, +.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-nav-bg: rgba(8, 15, 28, 0.92); + + /* ThunderID Design Tokens for Dark Mode */ + --thunderid-color-background-surface: #0f1a2d; + --thunderid-color-background-body: #080f1c; + --thunderid-color-background-disabled: rgba(255, 255, 255, 0.06); + --thunderid-color-background-muted: rgba(255, 255, 255, 0.05); + --thunderid-color-text-primary: #E0EAFF; + --thunderid-color-text-secondary: rgba(224, 234, 255, 0.65); + --thunderid-color-border: rgba(255, 255, 255, 0.09); + --thunderid-color-border-focus: var(--color-primary); + --thunderid-input-borderColor: rgba(255, 255, 255, 0.16); + --thunderid-input-focusBorderColor: var(--color-primary); + --thunderid-input-focusRing: 0 0 0 3px rgba(54, 136, 255, 0.25); + --thunderid-card-borderColor: rgba(255, 255, 255, 0.09); + --thunderid-card-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); + --thunderid-color-action-hover: rgba(255, 255, 255, 0.06); +} + +/* ── Navigation ── */ +.nav { + position: fixed; + inset-block-start: 0; + inset-inline: 0; + height: 52px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 24px; + background: var(--color-nav-bg); + 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 { + display: flex; + flex-direction: column; + gap: 1px; +} + +.wordmark-name { + font-size: 14px; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--color-text); + line-height: 1; +} + +.wordmark-sub { + font-size: 8.5px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-primary); + line-height: 1; +} + +.dark-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + background: transparent; + border: none; + border-radius: var(--radius-btn); + color: var(--color-text-muted); + cursor: pointer; + transition: color 0.15s, background 0.15s; +} + +.dark-toggle:hover { + color: var(--color-text); + background: rgba(54, 136, 255, 0.06); +} + +.nav-actions { + display: flex; + align-items: center; + gap: 8px; +} + +/* ── Nav back button ── */ +.nav-back-btn { + display: inline-flex; + align-items: center; + gap: 4px; + height: 32px; + padding: 0 10px; + border-radius: 6px; + border: none; + background: transparent; + color: var(--color-text-muted); + font-size: 13px; + font-family: var(--font-sans); + letter-spacing: -0.01em; + cursor: pointer; + text-decoration: none; + transition: background 0.2s, color 0.2s; +} + +.nav-back-btn:hover { + background: var(--color-border); + color: var(--color-text); +} + +/* ── Buttons ── */ +button { + font-family: var(--font-sans); + cursor: pointer; + border: none; + border-radius: var(--radius-btn); + font-weight: 500; + 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 { + background: var(--color-primary); + color: #ffffff; + border: 1.5px solid var(--color-primary); +} + +.btn-primary:hover { + background: var(--color-primary-hover); + border-color: var(--color-primary-hover); +} + +.btn-outline { + background: transparent; + color: var(--color-text); + border: 1.5px solid rgba(54, 136, 255, 0.4); +} + +.btn-outline:hover { + border-color: var(--color-primary); + color: var(--color-primary); +} + +.btn-ghost { + background: transparent; + color: var(--color-text-muted); + border: none; +} + +.btn-ghost:hover { + color: var(--color-text); + background: rgba(54, 136, 255, 0.06); +} + +.btn-sm { + padding: 6px 14px; + font-size: 13px; + height: 32px; +} + +.btn-lg { + padding: 0 24px; + font-size: 15px; + height: 46px; +} + +/* ── 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: 20px; + max-width: 560px; + width: 100%; + margin: 0 auto; +} + +.hero-badge { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-text-muted); +} + +.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; +} + +.hero-subtitle { + font-size: 16px; + color: var(--color-text-muted); + max-width: 420px; + line-height: 1.8; + letter-spacing: -0.01em; +} + +.hero-ctas { + display: flex; + gap: 12px; + flex-wrap: wrap; + justify-content: center; + margin-top: 4px; +} + +.hero-divider { + width: 100%; + border: none; + border-top: 1px solid var(--color-border); + margin: 8px 0; +} + +/* ── Stats row ── */ +.hero-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + width: 100%; + border: 1px solid var(--color-border); + border-radius: var(--radius-card); + overflow: hidden; +} + +.hero-stat { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 20px 16px; + 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; +} + +/* ── 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; + } +} + +/* ── Signed-in 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-greeting-text { + min-width: 0; +} + +.home-avatar { + width: 52px; + height: 52px; + border-radius: 50%; + flex-shrink: 0; + object-fit: cover; +} + +.home-avatar--fallback { + display: flex; + align-items: center; + justify-content: center; + background: rgba(54, 136, 255, 0.12); + color: var(--color-primary); + font-size: 18px; + font-weight: 700; + letter-spacing: -0.02em; +} + +.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-text-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: var(--font-mono, monospace); + letter-spacing: 0.01em; +} + +.home-stat-label { + font-size: 11px; + font-weight: 600; + color: var(--color-text-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-text-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-text-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; +} + +/* ── Sign in / Sign up pages ── */ +.auth-main { + width: 100%; + min-height: calc(100vh - 52px); + margin-top: 52px; + display: flex; + align-items: center; + justify-content: center; + padding: 40px 20px; +} + +.auth-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + background: var(--color-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-card); + box-shadow: var(--shadow-card); + padding: 40px 32px; + max-width: 420px; + width: 100%; +} + +.auth-title { + font-size: 1.5rem; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--color-text); + text-align: center; +} + +.auth-subtitle { + margin: 0 0 16px; + font-size: 13px; + color: var(--color-text-muted); + text-align: center; +} + +.auth-switch { + margin-top: 20px; + font-size: 13px; + color: var(--color-text-muted); + text-align: center; +} + +/* ── Profile page ── */ +.profile-main { + width: 100%; + max-width: 780px; + margin: 0 auto; + padding: 52px 28px 80px; +} + +/* ── Token debug page ── */ +.token-main { + width: 100%; + max-width: 780px; + margin: 0 auto; + padding: 52px 28px 80px; +} + +.token-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 36px; + flex-wrap: wrap; +} + +.token-title { + margin: 0 0 5px; + font-size: 22px; + font-weight: 700; + letter-spacing: -0.04em; + color: var(--color-text); +} + +.token-subtitle { + margin: 0; + font-size: 13px; + color: var(--color-text-muted); + letter-spacing: -0.01em; +} + +.token-badge { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 6px 12px; + border-radius: 6px; + font-size: 12px; + font-weight: 700; + letter-spacing: -0.01em; + flex-shrink: 0; +} + +.token-badge--expired { + background: rgba(220, 38, 38, 0.08); + border: 1px solid rgba(220, 38, 38, 0.22); + color: #dc2626; +} + +.token-badge--valid { + background: rgba(47, 189, 107, 0.08); + border: 1px solid rgba(47, 189, 107, 0.22); + color: #2fbd6b; +} + +.token-badge-dot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; + flex-shrink: 0; +} + +.token-raw-section { + margin-bottom: 20px; +} + +.token-raw-label-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.token-section-label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--color-text-muted); +} + +.token-copy-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 10px; + border-radius: 5px; + background: transparent; + border: 1px solid var(--color-border); + font-size: 11px; + font-weight: 600; + color: var(--color-text-muted); + cursor: pointer; + font-family: var(--font-sans); + letter-spacing: -0.01em; + transition: border-color 0.2s, color 0.2s; +} + +.token-copy-btn:hover { + border-color: #3688FF; + color: #3688FF; +} + +.token-raw { + display: block; + font-size: 11px; + font-family: var(--font-mono, monospace); + word-break: break-all; + line-height: 1.8; + background: #0b1120; + padding: 14px 16px; + border-radius: 8px; + overflow-x: auto; +} + +.token-raw--loading { + color: rgba(255, 255, 255, 0.3); +} + +.token-part--header { color: #ff7b72; } +.token-part--payload { color: #79c0ff; } +.token-part--signature { color: #3fb950; } + +.token-dot { + color: rgba(255, 255, 255, 0.2); +} + +.token-decoded-grid { + display: grid; + grid-template-columns: 1fr 1.7fr; + gap: 12px; + margin-bottom: 20px; +} + +.token-code-box { + background: #0b1120; + border-radius: 10px; + overflow: hidden; +} + +.token-code-box-header { + padding: 9px 14px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.09em; + text-transform: uppercase; +} + +.token-code-box-header--header { + color: #ff7b72; + border-bottom: 1px solid rgba(255, 123, 114, 0.15); +} + +.token-code-box-header--payload { + color: #79c0ff; + border-bottom: 1px solid rgba(121, 192, 255, 0.15); +} + +.token-code-pre { + margin: 0; + padding: 14px; + font-size: 11.5px; + line-height: 1.8; + font-family: var(--font-mono, monospace); + color: #c9d1d9; + overflow-x: auto; + max-height: 220px; +} + +.token-meta-row { + display: flex; + gap: 32px; + padding-top: 20px; + border-top: 1px solid var(--color-border); + flex-wrap: wrap; +} + +.token-meta-label { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--color-text-muted); + margin-bottom: 5px; +} + +.token-meta-value { + font-size: 12px; + font-family: var(--font-mono, monospace); + color: var(--color-text); + letter-spacing: -0.01em; +} + +/* ─── Account page (Google Account-style left-nav layout) ─────────── */ +.account-page { + display: flex; + align-items: flex-start; + min-height: calc(100vh - 52px); + margin-top: 52px; +} + +.account-sidebar { + width: 280px; + flex-shrink: 0; + padding: 40px 24px; + position: sticky; + top: 52px; +} + +.account-sidebar-title { + font-size: 22px; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--color-text); + margin: 0 0 24px 16px; +} + +.account-nav { + display: flex; + flex-direction: column; + gap: 2px; +} + +.account-nav-item { + display: flex; + align-items: center; + gap: 14px; + width: 100%; + padding: 11px 16px; + border: none; + border-radius: 20px; + background: transparent; + color: var(--color-text-muted); + font-size: 14px; + font-weight: 500; + font-family: inherit; + text-align: left; + cursor: pointer; + white-space: nowrap; + transition: background 0.2s, color 0.2s; +} + +.account-nav-item:hover { + background: var(--color-border); + color: var(--color-text); +} + +.account-nav-item--active, +.account-nav-item--active:hover { + background: rgba(54, 136, 255, 0.12); + color: var(--color-primary); +} + +.account-content { + flex: 1; + min-width: 0; + display: flex; + justify-content: center; + padding: 40px 32px 80px; + border-left: 1px solid var(--color-border); +} + +.account-content-inner { + width: 100%; + max-width: 720px; +} + +/* UserProfile's `min-width: 600px` (a floor meant to keep the field grid from getting + cramped in other contexts) fights this page's own responsive layout, refusing to shrink + below 600px even when the viewport is narrower. The Security/Home boxes have no such floor + and already shrink fluidly with their container, so match that here. */ +.account-content .thunderid-user-profile { + min-width: 0; + background: var(--color-card) !important; + border: 1px solid var(--color-border) !important; + border-radius: var(--radius-card) !important; + box-shadow: var(--shadow-card); + color: var(--color-text); + box-sizing: border-box; +} + +.account-content .thunderid-user-profile .thunderid-user-profile__hero { + border-bottom: 1px solid var(--color-border); +} + +.account-content .thunderid-user-profile .thunderid-user-profile__hero-name { + color: var(--color-text); +} + +.account-content .thunderid-user-profile .thunderid-user-profile__hero-subtitle { + color: var(--color-text-muted); +} + +.account-content .thunderid-user-profile .thunderid-user-profile__field { + border-bottom: 1px solid var(--color-border); +} + +.account-content .thunderid-user-profile .thunderid-user-profile__field-label { + color: var(--color-text-muted); +} + +.account-content .thunderid-user-profile .thunderid-user-profile__field-value { + color: var(--color-text); +} + +.account-content .thunderid-user-profile .thunderid-user-profile__field-placeholder { + color: var(--color-text-muted); +} + +.account-content .thunderid-user-profile .thunderid-user-profile__field-edit-btn { + color: var(--color-text-muted); +} + +.account-content .thunderid-user-profile .thunderid-user-profile__field-edit-btn:hover { + color: var(--color-primary); +} + +.account-content .thunderid-user-profile input, +.account-content .thunderid-user-profile select, +.account-content .thunderid-user-profile textarea { + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--color-border); + border-radius: var(--radius-btn); + color: var(--color-text); + padding: 6px 10px; +} + +.account-content-title { + font-size: 26px; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--color-text); + margin: 0 0 6px; +} + +.account-content-subtitle { + font-size: 14px; + color: var(--color-text-muted); + margin: 0 0 32px; +} + +.account-tiles { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + margin-top: 8px; +} + +.account-tile { + text-align: left; + background: var(--color-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-card); + padding: 22px; + cursor: pointer; + font-family: inherit; + display: flex; + flex-direction: column; + transition: border-color 0.2s; +} + +.account-tile:hover { + border-color: rgba(54, 136, 255, 0.35); +} + +.account-tile-icon { + width: 36px; + height: 36px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 14px; +} + +.account-tile-icon--blue { + background: rgba(54, 136, 255, 0.12); + color: var(--color-primary); +} + +.account-tile-icon--green { + background: rgba(16, 185, 129, 0.12); + color: #10b981; +} + +.account-tile-title { + font-size: 15px; + font-weight: 600; + color: var(--color-text); + margin-bottom: 4px; +} + +.account-tile-desc { + font-size: 12.5px; + color: var(--color-text-muted); + line-height: 1.5; +} + +.account-security-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.account-security-card { + background: var(--color-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-card); + padding: 20px 22px; +} + +.account-security-card-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.account-security-card-title { + font-size: 14px; + font-weight: 600; + color: var(--color-text); + margin-bottom: 2px; +} + +.account-security-card-desc { + font-size: 12.5px; + color: var(--color-text-muted); +} + +.account-security-card-cta { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 6px; + padding: 0; + background: none; + border: none; + color: var(--color-primary); + font-size: 13px; + font-weight: 500; + font-family: inherit; + cursor: pointer; + transition: opacity 0.2s; +} + +.account-security-card-cta:hover { + opacity: 0.8; +} + +.account-security-card-chevron { + display: inline-flex; + transition: transform 0.2s; +} + +.account-security-card-chevron--open { + transform: rotate(180deg); +} + +.account-security-card-form { + margin-top: 18px; + padding-top: 18px; + border-top: 1px solid var(--color-border); +} + +@media (max-width: 760px) { + .account-page { + flex-direction: column; + } + .account-sidebar { + width: 100%; + position: static; + padding: 20px 16px 8px; + } + .account-nav { + flex-direction: row; + overflow-x: auto; + } + .account-content { + max-width: 100%; + border-left: none; + border-top: 1px solid var(--color-border); + padding: 24px 20px 60px; + } +} + +/* ── ThunderID inputs & forms dark mode ── */ +.dark .thunderid-password-field__wrapper, +html.dark .thunderid-password-field__wrapper, +.app.dark .thunderid-password-field__wrapper, +.dark .thunderid-text-field__wrapper, +html.dark .thunderid-text-field__wrapper, +.app.dark .thunderid-text-field__wrapper, +.dark .thunderid-text-field__input, +html.dark .thunderid-text-field__input, +.app.dark .thunderid-text-field__input { + background-color: rgba(255, 255, 255, 0.05) !important; + border-color: rgba(255, 255, 255, 0.16) !important; + color: var(--color-text) !important; +} + +.dark .thunderid-password-field__label, +html.dark .thunderid-password-field__label, +.app.dark .thunderid-password-field__label, +.dark .thunderid-text-field__label, +html.dark .thunderid-text-field__label, +.app.dark .thunderid-text-field__label, +.dark .thunderid-input-label, +html.dark .thunderid-input-label, +.app.dark .thunderid-input-label { + color: var(--color-text) !important; +} + +.dark .thunderid-password-field__input, +html.dark .thunderid-password-field__input, +.app.dark .thunderid-password-field__input { + background: transparent !important; + color: var(--color-text) !important; +} + +.dark .thunderid-password-field__input::placeholder, +html.dark .thunderid-password-field__input::placeholder, +.app.dark .thunderid-password-field__input::placeholder, +.dark .thunderid-text-field__input::placeholder, +html.dark .thunderid-text-field__input::placeholder, +.app.dark .thunderid-text-field__input::placeholder { + color: var(--color-text-muted) !important; +} + +.dark .thunderid-password-field__toggle, +html.dark .thunderid-password-field__toggle, +.app.dark .thunderid-password-field__toggle { + color: var(--color-text-muted) !important; + background: transparent !important; +} + +.dark .thunderid-password-field__toggle:hover, +html.dark .thunderid-password-field__toggle:hover, +.app.dark .thunderid-password-field__toggle:hover { + color: var(--color-text) !important; +} + +.dark .thunderid-change-credential__requirements-heading, +html.dark .thunderid-change-credential__requirements-heading, +.app.dark .thunderid-change-credential__requirements-heading, +.dark .thunderid-change-credential__requirement, +html.dark .thunderid-change-credential__requirement, +.app.dark .thunderid-change-credential__requirement { + color: var(--color-text-muted) !important; +} + +.dark .thunderid-change-credential__requirement--passed, +html.dark .thunderid-change-credential__requirement--passed, +.app.dark .thunderid-change-credential__requirement--passed { + color: #38c172 !important; +} + +/* ── ThunderID UserDropdown dark mode ── */ +.dark .thunderid-user-dropdown__menu, +html.dark .thunderid-user-dropdown__menu, +.app.dark .thunderid-user-dropdown__menu, +.dark .thunderid-user-dropdown__content, +html.dark .thunderid-user-dropdown__content, +.app.dark .thunderid-user-dropdown__content { + background-color: var(--color-card) !important; + border: 1px solid var(--color-border) !important; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45) !important; + color: var(--color-text) !important; +} + +.dark .thunderid-user-dropdown__menu-header, +html.dark .thunderid-user-dropdown__menu-header, +.app.dark .thunderid-user-dropdown__menu-header, +.dark .thunderid-user-dropdown__header, +html.dark .thunderid-user-dropdown__header, +.app.dark .thunderid-user-dropdown__header { + border-bottom: 1px solid var(--color-border) !important; +} + +.dark .thunderid-user-dropdown__menu-header-name, +html.dark .thunderid-user-dropdown__menu-header-name, +.app.dark .thunderid-user-dropdown__menu-header-name, +.dark .thunderid-user-dropdown__header-name, +html.dark .thunderid-user-dropdown__header-name, +.app.dark .thunderid-user-dropdown__header-name { + color: var(--color-text) !important; +} + +.dark .thunderid-user-dropdown__menu-header-subtitle, +html.dark .thunderid-user-dropdown__menu-header-subtitle, +.app.dark .thunderid-user-dropdown__menu-header-subtitle, +.dark .thunderid-user-dropdown__header-subtitle, +html.dark .thunderid-user-dropdown__header-subtitle, +.app.dark .thunderid-user-dropdown__header-subtitle { + color: var(--color-text-muted) !important; +} + +.dark .thunderid-user-dropdown__menu-divider, +html.dark .thunderid-user-dropdown__menu-divider, +.app.dark .thunderid-user-dropdown__menu-divider, +.dark .thunderid-divider, +html.dark .thunderid-divider, +.app.dark .thunderid-divider { + background-color: var(--color-border) !important; + border-color: var(--color-border) !important; +} + +.dark .thunderid-user-dropdown__item, +html.dark .thunderid-user-dropdown__item, +.app.dark .thunderid-user-dropdown__item { + color: var(--color-text) !important; +} + +.dark .thunderid-user-dropdown__item:hover, +html.dark .thunderid-user-dropdown__item:hover, +.app.dark .thunderid-user-dropdown__item:hover { + background-color: rgba(255, 255, 255, 0.06) !important; + color: var(--color-text) !important; +} + +.dark .thunderid-user-dropdown__item--danger, +html.dark .thunderid-user-dropdown__item--danger, +.app.dark .thunderid-user-dropdown__item--danger { + color: #f87171 !important; +} + +.dark .thunderid-user-dropdown__item--danger:hover, +html.dark .thunderid-user-dropdown__item--danger:hover, +.app.dark .thunderid-user-dropdown__item--danger:hover { + background-color: rgba(239, 68, 68, 0.12) !important; +} + +.dark .thunderid-card, +html.dark .thunderid-card, +.app.dark .thunderid-card { + background-color: var(--color-card) !important; + border-color: var(--color-border) !important; + color: var(--color-text) !important; +} + diff --git a/quickstarts/nuxt/app/components/AppNav.vue b/quickstarts/nuxt/app/components/AppNav.vue new file mode 100644 index 0000000..91ac758 --- /dev/null +++ b/quickstarts/nuxt/app/components/AppNav.vue @@ -0,0 +1,82 @@ + + + diff --git a/quickstarts/nuxt/app/components/ConfigNotice.vue b/quickstarts/nuxt/app/components/ConfigNotice.vue new file mode 100644 index 0000000..52d595f --- /dev/null +++ b/quickstarts/nuxt/app/components/ConfigNotice.vue @@ -0,0 +1,112 @@ + + + diff --git a/quickstarts/nuxt/app/pages/account.vue b/quickstarts/nuxt/app/pages/account.vue new file mode 100644 index 0000000..addd18b --- /dev/null +++ b/quickstarts/nuxt/app/pages/account.vue @@ -0,0 +1,162 @@ + + + diff --git a/quickstarts/nuxt/app/pages/index.vue b/quickstarts/nuxt/app/pages/index.vue new file mode 100644 index 0000000..fe0b7ab --- /dev/null +++ b/quickstarts/nuxt/app/pages/index.vue @@ -0,0 +1,234 @@ + + + diff --git a/quickstarts/nuxt/app/pages/signin.vue b/quickstarts/nuxt/app/pages/signin.vue new file mode 100644 index 0000000..adcb418 --- /dev/null +++ b/quickstarts/nuxt/app/pages/signin.vue @@ -0,0 +1,24 @@ + + + diff --git a/quickstarts/nuxt/app/pages/signup.vue b/quickstarts/nuxt/app/pages/signup.vue new file mode 100644 index 0000000..6e8405e --- /dev/null +++ b/quickstarts/nuxt/app/pages/signup.vue @@ -0,0 +1,24 @@ + + + diff --git a/quickstarts/nuxt/app/pages/token.vue b/quickstarts/nuxt/app/pages/token.vue new file mode 100644 index 0000000..b450504 --- /dev/null +++ b/quickstarts/nuxt/app/pages/token.vue @@ -0,0 +1,125 @@ + + + diff --git a/quickstarts/nuxt/nuxt.config.ts b/quickstarts/nuxt/nuxt.config.ts new file mode 100644 index 0000000..229cee9 --- /dev/null +++ b/quickstarts/nuxt/nuxt.config.ts @@ -0,0 +1,11 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + compatibilityDate: '2025-07-15', + devtools: { enabled: true }, + modules: ['@thunderid/nuxt'], + thunderid: { + // Serves POST /api/auth/backchannel-logout, so signing out elsewhere ends the session here. + backchannelLogout: { enabled: true }, + }, + css: ['~/assets/styles.css'], +}) diff --git a/quickstarts/nuxt/package.json b/quickstarts/nuxt/package.json new file mode 100644 index 0000000..5975875 --- /dev/null +++ b/quickstarts/nuxt/package.json @@ -0,0 +1,18 @@ +{ + "name": "@thunderid/nuxt-quickstart", + "private": true, + "version": "0.0.0", + "scripts": { + "prepare-dev": "node scripts/prepare-dev.cjs", + "prepare-dev:redirect": "node scripts/prepare-dev.cjs --flow=redirect", + "dev": "TMPDIR=/tmp/nuxt-tmp/ nuxt dev", + "build": "nuxt build", + "generate": "nuxt generate", + "preview": "nuxt preview" + }, + "dependencies": { + "@thunderid/nuxt": "1.3.0", + "nuxt": "^4.5.2", + "vue": "^3.5.13" + } +} diff --git a/quickstarts/nuxt/pnpm-lock.yaml b/quickstarts/nuxt/pnpm-lock.yaml new file mode 100644 index 0000000..ae7a14d --- /dev/null +++ b/quickstarts/nuxt/pnpm-lock.yaml @@ -0,0 +1,6358 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@thunderid/nuxt': + specifier: 1.3.0 + version: 1.3.0(magicast@0.5.5)(nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.154.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.9))(rollup@4.64.9)(srvx@1.0.6)(terser@5.51.2)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(vue-router@5.4.0(@vue/compiler-sfc@3.5.44)(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44))(vue@3.5.44) + nuxt: + specifier: ^4.5.2 + version: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.154.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.9))(rollup@4.64.9)(srvx@1.0.6)(terser@5.51.2)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + vue: + specifier: ^3.5.13 + version: 3.5.44 + +packages: + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.29.7': + resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.29.7': + resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.29.7': + resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.29.7': + resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.29.7': + resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@8.0.6': + resolution: {integrity: sha512-+6HYEBnsDRZHJtUTAEfMQd0zMDqdFHS7C+5vAsEZqBUr3vEMWOWlHJ0Jz1gS8jJIlCvLsuA6ivmeoqzH936YcA==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@8.0.6': + resolution: {integrity: sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.10': + resolution: {integrity: sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@8.0.7': + resolution: {integrity: sha512-UkDwEbPO1px2JQSrdh4qIgaH1JgG7GO/pFeJkpxhDxIETkT/QvGpnigXIw75ViHYPtIgKITmVUJqLjiO+JuWjA==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.29.7': + resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.29.9': + resolution: {integrity: sha512-FFwIwzU+7SCOuxxV4YtJql6T9981ZVTm+FHO5GhVsRqCTdy0WwrEZh3l42ARpXruJUDGOptdduHW6Zr7pNPLNg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.10': + resolution: {integrity: sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@babel/types@8.0.6': + resolution: {integrity: sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@clack/core@1.5.1': + resolution: {integrity: sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==} + engines: {node: '>= 20.12.0'} + + '@clack/prompts@1.8.1': + resolution: {integrity: sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==} + engines: {node: '>= 20.12.0'} + + '@cloudflare/kv-asset-handler@0.4.2': + resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} + engines: {node: '>=18.0.0'} + + '@colordx/core@7.3.0': + resolution: {integrity: sha512-LVzTTPhX7wpy7TkGjouA9zbbCBIw92RGm7ZQqSKFE0oYE3sMx2FGXz90eVpgw7k/+wrrY+qJ86nHXifZWMS5jA==} + + '@csstools/css-tokenizer@4.0.2': + resolution: {integrity: sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==} + engines: {node: '>=20.19.0'} + + '@dxup/nuxt@0.5.10': + resolution: {integrity: sha512-54Vehb7LmR7qYpC6KFwjDTSm6CB1CayBu+JXdHrPLrIjjr5xAgb43jvgOU+mnB+tsRh414fSsEy0QjBMKILQ8g==} + + '@dxup/unimport@0.1.2': + resolution: {integrity: sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ==} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@ioredis/commands@1.10.0': + resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@mapbox/node-pre-gyp@2.0.3': + resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} + engines: {node: '>=18'} + hasBin: true + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nuxt/cli@4.0.1': + resolution: {integrity: sha512-vtB77CmkrYUMoIKwk+HUdNQX1vuMPpnu7ExNYB91WuIxzzGrGQLSxGUpXa360xsyUkZ4h9s+qElAQQW9ZQP4WA==} + engines: {node: ^22.21.0 || ^24.11.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@nuxt/docs': ^4.0.0 + '@nuxt/schema': ^3.0.0 || ^4.0.0 + jiti: ^2.3.0 + oxc-parser: '>=0.56.3' + rolldown: ^1.0.0-rc.16 || ^1.0.0 + serve: ^14.0.0 + peerDependenciesMeta: + '@nuxt/docs': + optional: true + '@nuxt/schema': + optional: true + jiti: + optional: true + oxc-parser: + optional: true + rolldown: + optional: true + serve: + optional: true + + '@nuxt/devalue@2.0.2': + resolution: {integrity: sha512-GBzP8zOc7CGWyFQS6dv1lQz8VVpz5C2yRszbXufwG/9zhStTIH50EtD87NmWbTMwXDvZLNg8GIpb1UFdH93JCA==} + + '@nuxt/devtools-kit@3.4.3': + resolution: {integrity: sha512-mG4OT+1v6JkaRj+L9bYI6TaWLqDwu4NE+//UL/annLsrGiAOXc9FlFAnIIuQojPZ+H1gJeuc1pc968z/jXwUlA==} + peerDependencies: + vite: '>=6.0' + + '@nuxt/devtools-wizard@3.4.3': + resolution: {integrity: sha512-0F5mQmaj3rEMT5gkQeDsXea7YyyvPwp4ktT3xduZgp1G0QiSzV7AQ64gXz2LPaIc4V8ib/k++8KjmHL+/DbH4Q==} + hasBin: true + + '@nuxt/devtools@3.4.3': + resolution: {integrity: sha512-63qNER1zq1z+b+zUms2X9xKu8SV04FSab8C7yqzCy/8C/mPodNVJZsI6YsCXhz2BLAM1E9+3vIPzLT8BJjKKmA==} + hasBin: true + peerDependencies: + '@vitejs/devtools': '*' + vite: '>=6.0' + peerDependenciesMeta: + '@vitejs/devtools': + optional: true + + '@nuxt/kit@3.21.11': + resolution: {integrity: sha512-0Xi3tgwN77w43Q8GCPIrvWmF1J7Peehkts44E0uKNIml9lB8WoUn8YxyUjxBv47XtVR86NWoWALAT+/IEMHJEA==} + engines: {node: '>=18.12.0'} + + '@nuxt/kit@4.6.2': + resolution: {integrity: sha512-Vn9DJobc30WdR1vrlRcEAFSFy7NV5CWq3o6gxsfrdGb2FmCh0fuDyvbZC30rm2enyiPaOUPafKctFE/ToBjybw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@nuxt/schema': ^3.0.0 || ^4.0.0 || ^5.0.0-0 + confbox: '>=0.1.0' + dotenv: '*' + giget: '>=3.1.0' + jiti: ^2.0.0 + mlly: ^1.2.0 + oxc-parser: '>=0.56.0' + rolldown: '>=1.0.0' + peerDependenciesMeta: + '@nuxt/schema': + optional: true + confbox: + optional: true + dotenv: + optional: true + giget: + optional: true + jiti: + optional: true + mlly: + optional: true + oxc-parser: + optional: true + rolldown: + optional: true + + '@nuxt/nitro-server@4.6.2': + resolution: {integrity: sha512-mOiTpoiSG26/0UI4Nxfc4AK76UaCdpjObLPCF6NTDX9wL6Ed4/95AAVZKbEq7qxtNSla/8F+s8NSY6O1wpJ5bA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + peerDependencies: + '@babel/plugin-proposal-decorators': ^7.25.0 || ^8.0.0 + '@babel/plugin-syntax-typescript': ^7.25.0 || ^8.0.0 + '@rollup/plugin-babel': ^6.0.0 || ^7.0.0 + nuxt: ^4.6.2 + peerDependenciesMeta: + '@babel/plugin-proposal-decorators': + optional: true + '@babel/plugin-syntax-typescript': + optional: true + '@rollup/plugin-babel': + optional: true + + '@nuxt/schema@4.6.2': + resolution: {integrity: sha512-L+0YiNDh/5C079JiHweAbOJqWD/931N+aqwtRk/wrCpRmTmYgiyIelVbnOv0sevWvGcPouBJRgTC2fxlp3mxoQ==} + engines: {node: ^14.18.0 || >=16.10.0} + + '@nuxt/telemetry@2.9.1': + resolution: {integrity: sha512-WYQs6GvebU278YXhbGlLA8pUO3ox+juHAiKCB3SEH0lq2PDRwX465tHRtpVIqsHv+lGK/n99FC7qqyBt9+p3oQ==} + engines: {node: '>=18.12.0'} + hasBin: true + peerDependencies: + '@nuxt/kit': '>=3.0.0' + + '@nuxt/vite-builder@4.6.2': + resolution: {integrity: sha512-UCEoVjguX65x2mlZ0wuTth//idqugfKkxK9RwXHUqCkxonO4YgVh9akqUUlcjw/CSWWOZUbAOFnyS1yIXNGOZw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + peerDependencies: + '@babel/plugin-proposal-decorators': ^7.25.0 || ^8.0.0 + '@babel/plugin-syntax-jsx': ^7.25.0 || ^8.0.0 + nuxt: 4.6.2 + rolldown: ^1.0.0 + rollup-plugin-visualizer: ^6.0.0 || ^7.0.1 + vue: ^3.3.4 + peerDependenciesMeta: + '@babel/plugin-proposal-decorators': + optional: true + '@babel/plugin-syntax-jsx': + optional: true + rolldown: + optional: true + rollup-plugin-visualizer: + optional: true + + '@nuxt/vite-server@4.6.2': + resolution: {integrity: sha512-tSlCRgrx8TFBhETV/6dFk1gKLmwj1GaIrItRJClqJesJvdGDAabA3av4Ph8wlFGD5vkxIvzAeksn8Uy1ja1DMQ==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + peerDependencies: + nuxt: ^4.6.2 + vite: ^8.3.4 + + '@oxc-project/types@0.154.0': + resolution: {integrity: sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==} + + '@parcel/watcher-wasm@2.6.0': + resolution: {integrity: sha512-dtjbDxKSDPQ8AmA+pS4OFaHE1FKrjtGpLGBxw85uKFkRorjNbvDM/aFPgqosu40wprbp1xw2ZSxIKqghCUHe2w==} + engines: {node: '>= 10.0.0'} + bundledDependencies: + - napi-wasm + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.7.0': + resolution: {integrity: sha512-0UTYalzk2t6S4rA2uHOz5bSSW2CHdv4vggJI6Alg90yvl0UgXs6XSXpH96OH+bRkX4J/06djv29pqXJ0lq5Kag==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + + '@rolldown/binding-android-arm-eabi@1.2.14': + resolution: {integrity: sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.14': + resolution: {integrity: sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.14': + resolution: {integrity: sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.14': + resolution: {integrity: sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.14': + resolution: {integrity: sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.14': + resolution: {integrity: sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.14': + resolution: {integrity: sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.14': + resolution: {integrity: sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.14': + resolution: {integrity: sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.14': + resolution: {integrity: sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.14': + resolution: {integrity: sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.14': + resolution: {integrity: sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.14': + resolution: {integrity: sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.14': + resolution: {integrity: sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.14': + resolution: {integrity: sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rollup/plugin-alias@6.0.0': + resolution: {integrity: sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g==} + engines: {node: '>=20.19.0'} + peerDependencies: + rollup: '>=4.0.0' + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-commonjs@29.0.3': + resolution: {integrity: sha512-ZaOxZceP7SOUW7Lqw5IRVweSQYWaeIPnXIGLiB690EBA3FGJTO40EEr2L5yZplJWsgTCogILRSpcAe7+U0Otdg==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-inject@5.0.5': + resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-json@6.1.0': + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-node-resolve@16.0.3': + resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-replace@6.0.3': + resolution: {integrity: sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-terser@1.0.0': + resolution: {integrity: sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + rollup: ^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.64.9': + resolution: {integrity: sha512-qJmhckvOleXp8R+BaXiVW/S34/3M7pvN17/QcG90A74BDaSvKv8ffY+GYG+a/xU0vEoITzdMSiM7o9HZSQWbfA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.64.9': + resolution: {integrity: sha512-dqQgds39BNWGB2ughJ6n2LTQFXk8RAOwnXj6m387OHpygfD93QRS1ODIfpiWyAa9gsFB9l34pSqeLmA0Xk/VBw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.64.9': + resolution: {integrity: sha512-scrMx4SGikOSbWknLmPD0v6OeJ1Wih3kyRLdNZnlVbBeKVWp1nRTVcpM0IxDJdc3u8UjesPHTmnX649u8cQpBw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.64.9': + resolution: {integrity: sha512-fkwi2OQR+QWoViRFt08Bsca6G0e9pk/4z+JgmqHq6jSRTT0LtI27KRVK8bu++6L/CBtQ3OZePbqC3bcrwGzTqQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.64.9': + resolution: {integrity: sha512-8Apr0Khn0r2MUsDtcOMbp85fDdXsX/E+iLhO17WUjG24m3O8ChArqI0isI7CAdj9Ouq177PHtVtEUpMNE1C4Gw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.64.9': + resolution: {integrity: sha512-H+upy4DhBcBZlUsCAMyr8wR0Dg9kj8sGloVZdZ7va2heJM/5jnROxI47dtXseJjlD/VoQe6MrXhY3b5ikOq/Vw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.64.9': + resolution: {integrity: sha512-b50NS9TbAEK/b16vBoKrkeNwy+HvGBXmtBbzX4wcodWOh9dQZvRTN9rUyElIXa36QnwXhU9PAVVkZpqRzSi2Tg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.64.9': + resolution: {integrity: sha512-SdqcudB/FzjLESsRMgXhV+FXGtf6usxXwlMoRW7D/XS3f9CZ4T4wycRBu2h9bVl2AOaPpunoA8ftAM/MF4altQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.64.9': + resolution: {integrity: sha512-dg19FFRoovN+KZ+gMHBAq4Do8PgQmBcxpuhux57OuiR0QLavfn8uPBY8CP+UO50bO+xf0tK0GlqI/jld++5E6g==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.64.9': + resolution: {integrity: sha512-+jyv5ofGmpAvtrzLjSr/e4syskeikEp5L82bUzB76auwOgK1VNVwZV/F2g1Pes9bhTznbejOefAqPglHSKYmTA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.64.9': + resolution: {integrity: sha512-MKFXxqmkzCbtNvx/vjiJdss44eRtVabhCtDWcCFD9NHGwP+YkqnM/slnEsv/3fsoq9ukTtudraAjGhptrUPCVg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.64.9': + resolution: {integrity: sha512-lhARcOT6n7Ni9hpQRq8LZjbuoW+G/GXaW4Jfhx2MbmjnCj9XxLMRTceJIMpiMLQVWm+qxe7oq1sYvlIGOAW34Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.64.9': + resolution: {integrity: sha512-oORQzGxZ1oHghJwLdwc8Ni0SY9otILAeXsWdiImTF4F5HkWJUwh5s/269PF5tYILNC1z/3RwFsrgcHPguBcb0g==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.64.9': + resolution: {integrity: sha512-J2JQHQt7oR1uutd2OSwbV2Qpb2LI5gggPolbkUwDe7vn/0dnduZ0w6EIbGnZUAw7uIKaMgfVylcH6V+Aov8kRA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.64.9': + resolution: {integrity: sha512-bWaKLrfzeQ9mOzJJxXbPgw2RH6VAaTJrb/60Tqf7YSMxubG8780Hzm7Us+D6qVW8QRGJYl4VUvVXxnRB0o9zvQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.64.9': + resolution: {integrity: sha512-TFJyaCB9NsvSyvVGQgTSRD9XgxsAq99ivDDkt9KAySlrNW5Obsd1kQpvoCvBCOLq+JlJTjmtroLH0wp8M8mEsA==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.64.9': + resolution: {integrity: sha512-DvmOM5xz/wDnUJFVjqP32ahalxn7BDuvO25bpuFOrb50AIFCCcFPN4lWWUDD1c8Ddv9sMO9SXyqtOet8G8u9cQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.64.9': + resolution: {integrity: sha512-aFzokHK3yE00mzAvKm/PY76OsOOTjvoTGE82Vp89iBDyD2wUBlV4ZsR2tWkRECTEFk6TC4Yc3zaEWjPqq1vstQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.64.9': + resolution: {integrity: sha512-t/yMdeaaQUuNNpmT7taBmH2VvHzgT4YA/IaKVE3qVr4PYQemn/kmJkL4uG4UvTLcWXQd0MSoOa/3URYSWGmUEg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.64.9': + resolution: {integrity: sha512-4dnxKQ8GHARyjqAU87q5UWhywZTpIbR2VWhtACdC37GD1SGszmvb3TaBFVA4n+fnDHSKMut4ZJ63hVFwgnxVtg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.64.9': + resolution: {integrity: sha512-1qdQj6IrNoR0xFvrCQHmhBXe9HMbBGNTFi+3n4jKMt2rTx4k0aeAdraZU4WDOMfL9tAwS8/TRSuquvNuPQ+Gpw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.64.9': + resolution: {integrity: sha512-yc1GSEHIYhKGN5WSaJc+eMy8bZvzcvAmflEgFh8L3xcy8dYxGmfrudyBnBFde9f6JzOa1kFqocF8BHaWybjQoA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.64.9': + resolution: {integrity: sha512-VzHGvosAkLIzrwqvSgJBPDmGFTF/yE8OtMT4H5z8pM1NA0NiPpvTYnUh+9xPd+d5oy/wqF317FmJ1I8Kk8pp0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.64.9': + resolution: {integrity: sha512-FB77txy1PiAcJgG72nOLA6R0S3PRZahs8Spx0z2yseDUkoMmJcrQghiebJ2JHZ74cCOVlX/MYd4dUbJtJJ8fow==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.64.9': + resolution: {integrity: sha512-IoIgQLpK1DbOcQf1vX+i5oe5LXypsCVb1j/mU6BLAbOGmXH4E+j4kMfgZRwPYUohVBurNyVMgIKVM5dR6xA0kA==} + cpu: [x64] + os: [win32] + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.24': + resolution: {integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==} + + '@thunderid/browser@1.3.0': + resolution: {integrity: sha512-O6DHlUyVFfBaNcJDEXACLU4NJMrNjDlqAuzesvJ+8a0Gl5g8WFK53rpx027e/rWICG0vcGe2UVZuhvZq5Z511A==} + + '@thunderid/javascript@1.4.0': + resolution: {integrity: sha512-qHfIFzXOQTN1nhuBT91Tr8bLPvfyEDa7gUQSL7GNcJkcyyKfJ77meIwr91lCme0ySFUnTeKl0Bk+R8iIJiKXBw==} + + '@thunderid/node@1.3.0': + resolution: {integrity: sha512-hAFf4Bv3JZTcatF7Y/iarbd3lYLbIkz6SHzGKG5HOwh/a/Ip8qpu7u4npF6/0cNzrvZzY5g6awIymsIav75Xqw==} + + '@thunderid/nuxt@1.3.0': + resolution: {integrity: sha512-Vx+gkFQd0xShBED6sRLOr8hCz2oqmoD+q9P5+clGM5ptVFHxJL7Qveo+7VGgvSwpjfsUsn5kGwDoLfBGX5QbBA==} + peerDependencies: + nuxt: '>=3.10.0' + vue: '>=3.5.0' + + '@thunderid/vue@1.4.0': + resolution: {integrity: sha512-a/38OLJOb7vfyTgRsrbxh1SaDpoWRok9dg0nHO0KPjy5uKSHIEFKaPgbthd879zuuC0Pfm1jcovr3E5gmjtZ0w==} + peerDependencies: + vue: '>=3.5.0' + vue-router: '>=4.0.0' + peerDependenciesMeta: + vue-router: + optional: true + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@unhead/bundler@3.4.3': + resolution: {integrity: sha512-noUTzazRB4fMgO/nluoirrZJXngNbNb3sQzIslA805tKkwakRv2fib4Xe4EdiTuNB7V5XmpUFvfQkKRdBQ9qAA==} + peerDependencies: + '@unhead/cli': ^3.4.3 + '@vitejs/devtools-kit': ^0.7.6 + esbuild: '>=0.17.0' + lightningcss: '>=1.20.0' + oxc-parser: '>=0.98.0' + rolldown: '>=1.0.0' + unhead: ^3.4.3 + vite: '>=6.4.2' + webpack: '>=5.0.0' + peerDependenciesMeta: + '@unhead/cli': + optional: true + '@vitejs/devtools-kit': + optional: true + esbuild: + optional: true + lightningcss: + optional: true + oxc-parser: + optional: true + rolldown: + optional: true + vite: + optional: true + webpack: + optional: true + + '@unhead/vue@3.4.3': + resolution: {integrity: sha512-Wrmk62oC/V1EcBSKRDuy77kVaBtBXXsj6b+vEEWvmfo9AQfpLlc2b6vvvSj9WnXr8VY1ksKHj49Ejo8J/wLSOA==} + peerDependencies: + vite: '>=6.4.2' + vue: '>=3.5.18' + webpack: '>=5.0.0' + peerDependenciesMeta: + vite: + optional: true + webpack: + optional: true + + '@vercel/nft@1.11.0': + resolution: {integrity: sha512-m1QFg+U+3yPOnP1xSYJ73UIRxLOXdts1JOhiOiyPYqEsALgrXFFINvgUaD6R6iNvaBFAjHllBCbkfx4FuOdpaA==} + engines: {node: '>=20'} + hasBin: true + + '@vitejs/plugin-vue-jsx@5.1.6': + resolution: {integrity: sha512-YXvi4as2clxt6DFw5+a0tTA97ntiQXm/raR8ofNj3aNwwdlVGTiG2gp7EvfZW17P50acL/9bP0ccF4XnqNmlgA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.0.0 + + '@vitejs/plugin-vue@6.0.10': + resolution: {integrity: sha512-b80aUQh3n+qEzzZm5oQp3rT2QImeCx58K6eFl/RkJ3d65KW0kTuv5lNsLRVAjbW9YRUpo/wzmvzMgV34bTAItA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 || ^3.6.0-rc.10 + + '@vue-macros/common@3.1.4': + resolution: {integrity: sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==} + engines: {node: '>=20.19.0'} + peerDependencies: + vue: ^2.7.0 || ^3.2.25 + peerDependenciesMeta: + vue: + optional: true + + '@vue/babel-helper-vue-transform-on@2.0.1': + resolution: {integrity: sha512-uZ66EaFbnnZSYqYEyplWvn46GhZ1KuYSThdT68p+am7MgBNbQ3hphTL9L+xSIsWkdktwhPYLwPgVWqo96jDdRA==} + + '@vue/babel-plugin-jsx@2.0.1': + resolution: {integrity: sha512-a8CaLQjD/s4PVdhrLD/zT574ZNPnZBOY+IhdtKWRB4HRZ0I2tXBi5ne7d9eCfaYwp5gU5+4KIyFTV1W1YL9xZA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@2.0.1': + resolution: {integrity: sha512-ybwgIuRGRRBhOU37GImDoWQoz+TlSqap65qVI6iwg/J7FfLTLmMf97TS7xQH9I7Qtr/gp161kYVdhr1ZMraSYQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.44': + resolution: {integrity: sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ==} + + '@vue/compiler-dom@3.5.44': + resolution: {integrity: sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg==} + + '@vue/compiler-sfc@3.5.44': + resolution: {integrity: sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw==} + + '@vue/compiler-ssr@3.5.44': + resolution: {integrity: sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA==} + + '@vue/devtools-api@8.2.1': + resolution: {integrity: sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==} + + '@vue/devtools-core@8.2.1': + resolution: {integrity: sha512-s/VfAY9oDTb/kFEWmy461jaFde2MIV1RO/gi1vwM+PAZBZ/Pc2Ndu3BNBdZUze8QDUuyYvElbEEGA83syjJfzA==} + peerDependencies: + vue: ^3.0.0 + + '@vue/devtools-kit@8.2.1': + resolution: {integrity: sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==} + + '@vue/devtools-shared@8.2.1': + resolution: {integrity: sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==} + + '@vue/reactivity@3.5.44': + resolution: {integrity: sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw==} + + '@vue/runtime-core@3.5.44': + resolution: {integrity: sha512-uEa/ndwjhrrhnOfb2p+sZy6zeoamJOISqaLSJGk/8R6nABT1ZldpzcJoPzKj+zHVomVBPcmQYyC2r4LtvDV6CQ==} + + '@vue/runtime-dom@3.5.44': + resolution: {integrity: sha512-3fc4ARcXVhVBaJ3ujWiNZJARD6pkyRcwG+ZYhMzjKtKUp7MAY0HLwr7r3Smep3Li6ZaWT6ewdmx8XqdaD5oNZg==} + + '@vue/server-renderer@3.5.44': + resolution: {integrity: sha512-R4S1UoH4h5Frrauv8Z5XE5TxV9oBiUSgWIC4TyVK1FK+RocRnOLwFA5Xcmo2011RlgJp+AbZCnw6afrhlgRuUQ==} + + '@vue/shared@3.5.44': + resolution: {integrity: sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg==} + + abbrev@3.0.1: + resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} + engines: {node: ^18.17.0 || >=20.5.0} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn@8.19.0: + resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.4.0: + resolution: {integrity: sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@4.4.0: + resolution: {integrity: sha512-9k3v7xcHwgdO/DruxGIg4HtjvlAZlcnsX/mzqUb1t3NkYnl9kK2UJ+Gq0io+vQf7iT//BD/HB/NBkUR1LWxoeA==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + + args-tokenizer@0.3.0: + resolution: {integrity: sha512-xXAd7G2Mll5W8uo37GETpQ2VrE84M181Z7ugHFGQnJZ50M2mbOv0osSZ9VsSgPfJQ+LVG0prSi0th+ELMsno7Q==} + + ast-kit@2.2.0: + resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} + engines: {node: '>=20.19.0'} + + ast-kit@3.0.0: + resolution: {integrity: sha512-8OG92q3R35qjC/4i6BLBMg8IB+fClWu/1PEwg2Z9Rn+BuNaiEgJzpzn+pxWOdHJWDCAwu2JP0wCDTozAM4QirQ==} + engines: {node: ^22.18.0 || >=24.11.0} + + ast-walker-scope@0.10.0: + resolution: {integrity: sha512-KzvoxDfXNz2Gm8KYLh47K3qcY3Zza3WKOuh2xoqa7LBu6MDKWAZLovJtU9DuAd/Er+rhQHCRn619lHS5U+qmhw==} + engines: {node: ^22.18.0 || >=24.11.0} + + async-sema@3.1.1: + resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + autoprefixer@10.6.1: + resolution: {integrity: sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + b4a@1.9.0: + resolution: {integrity: sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + bare-events@2.9.2: + resolution: {integrity: sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.8.4: + resolution: {integrity: sha512-MPC2eyrpbk3A873sK4y6rk+Iv4o2enxTjHZ8zH3HO0mjrpm5XjnguY2XmfidIsDV6vnjpbMBKUAJpV0ISD2SsQ==} + engines: {bare: '>=1.28.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-path@3.1.2: + resolution: {integrity: sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==} + + bare-stream@2.13.4: + resolution: {integrity: sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.5.5: + resolution: {integrity: sha512-iM9qJCruvpt2rPaLsrumVDru9pogI7PHziHIa9kTNRFt8C8HuSa+kI9kFezK+NvHEFnnz71rKGZ1EAilq/3V/Q==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + + baseline-browser-mapping@2.11.29: + resolution: {integrity: sha512-YeesxhUmR4h5y5V8KF8iBCqJb8YFrBJ++XBTeGcWIaQOYXGN19JM0FoiOjAhwuDoln8owMgXcWBmgHtYSkky2w==} + engines: {node: '>=6.0.0'} + hasBin: true + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + birpc@4.2.0: + resolution: {integrity: sha512-KxgKcZPfrtzJDDALHPguGpGJUrzdgpymyiQQgzFjWreHMOpWrnFNVREr5J48x2DBh8ZVioscrV1SBkDipGiX+Q==} + + brace-expansion@2.1.7: + resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==} + + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.29.3: + resolution: {integrity: sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bundle-name@4.1.1: + resolution: {integrity: sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==} + engines: {node: '>=18'} + + c12@3.3.4: + resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + + cac@7.1.0: + resolution: {integrity: sha512-LMDU/U2yA4IPIY8KT0NsNPWEYZvAMkbgyspxJ6btJaYV+WMDOHAZCmB0lsTObiHaRS1V/ztKAl7+TSkTqXyeoA==} + engines: {node: '>=20.19.0'} + + caniuse-api@4.0.0: + resolution: {integrity: sha512-B0hQ1OLyJuHTQSOWXvwibWqM6DCoqJdvBA6X1S/53bd4XU7LJ1yurIPlrsouol3mw1jh9pGI4ivubSpmJeIqCA==} + + caniuse-lite@1.0.30001816: + resolution: {integrity: sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw==} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + citty@0.2.2: + resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} + + clickable-path@0.1.1: + resolution: {integrity: sha512-ROn/mdV2pR8WFdpJjFDCSouLRg9myf95xacO1iy8G/AVO/x5y124pjF3EaoWrTBRWC3vh0DPqa/MhKyx3mMeKg==} + engines: {node: '>=22.1.0'} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + cluster-key-slot@1.1.1: + resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} + engines: {node: '>=0.10.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + compatx@0.2.0: + resolution: {integrity: sha512-6gLRNt4ygsi5NyMVhceOCFv14CIdDFN7fQjX1U4+47qVE/+kjPoXMK65KWK+dWxmFzMTuKazoQ9sch6pM0p5oA==} + + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + confbox@0.3.1: + resolution: {integrity: sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-es@1.2.3: + resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} + + cookie-es@2.0.1: + resolution: {integrity: sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==} + + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + + core-js@3.42.0: + resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + + croner@10.0.1: + resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==} + engines: {node: '>=18.0'} + + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + + crossws@0.4.15: + resolution: {integrity: sha512-NsUOpDZosz7nyI+qEIl05yaiXOWaXH2oTeM+XBWtiSWZtb5rzcIsl1AST/HzM2jzUn76eFuyrkn83E6iSBRUvw==} + peerDependencies: + srvx: '>=0.11.5' + peerDependenciesMeta: + srvx: + optional: true + + cssnano@9.9.0: + resolution: {integrity: sha512-JS914Th2NRk2979JHJvbE3UjPtLmyXtPnr23NNul9BOQwEt9r/A2aM1STrKtKHAPc3XRaUDwMoqkXnjEpgHkAw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0} + peerDependencies: + postcss: ^8.5.29 + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + db0@0.3.4: + resolution: {integrity: sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw==} + peerDependencies: + '@electric-sql/pglite': '*' + '@libsql/client': '*' + better-sqlite3: '*' + drizzle-orm: '*' + mysql2: '*' + sqlite3: '*' + peerDependenciesMeta: + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + better-sqlite3: + optional: true + drizzle-orm: + optional: true + mysql2: + optional: true + sqlite3: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.1: + resolution: {integrity: sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + defu@6.1.5: + resolution: {integrity: sha512-pwdBJxJuJXmqrLO6s0VBmfbRz+G7FUzkjldAsdi9Yrv86mPyzq0ll1o8+8gB4Gsr6GJHbK1Lh3ngllgTInDCjA==} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@6.0.3: + resolution: {integrity: sha512-3woMEITftG1wKeO/4d/QXH1DVKctTSZg3jNGPpjp9hNr5Hos8MCFF5McTML5ZtgKDObm2R591B0spRp2IVernA==} + engines: {node: '>=18'} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + dom-walk@0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + + dompurify@3.4.13: + resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==} + + dot-prop@10.2.0: + resolution: {integrity: sha512-BTJ9aZYL3vCfZlZOBLy9v8TUqWGQ0pzFnygKwFZt5udj6viBoFIBviKPUoZLDCPn1FoXffv6McQFDenrm5Krfw==} + engines: {node: '>=20'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.459: + resolution: {integrity: sha512-4SC9HHSFjO/liAhifpdOL5hG9b4BP7/5G3y3UPZVO8trlAXr1CaUEEJL0gWK3kOcYcZrOA5rkwG45Gra/lTIgg==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + error-stack-parser-es@2.0.1: + resolution: {integrity: sha512-J36ntO+rMQVRuR/umlmxmfLi4TpWwtmTnHoJoXTiC2xDNazs0VDPKcX6pbhZMDd2HFtH9isMBJXMdbki+A++Pg==} + + errx@0.1.2: + resolution: {integrity: sha512-chfpPHmCerdo/rXr/nNvPZRkV4WwDRwzwnsJ0Uzz3tVi8Z41tDctRjduYy1138ii77AFlts1qvWtX3g/Acg91Q==} + + errx@0.2.2: + resolution: {integrity: sha512-HaPN6cxm6iZ9bIbQQ5orChcw7SY5smUzM08ZMc6OtV4UQhrrc3Ms1G8Ww2n/9lXkWqiioNTssF8bCvchrSbFEQ==} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + exsolve@1.1.3: + resolution: {integrity: sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA==} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-npm-meta@2.2.0: + resolution: {integrity: sha512-99jPl8JkCSCa4VlboNU1XuL98ijm74Pm9CGo6H4BoMVoVh1uhguQcvwLgXDT8Vkl2qj/UEQ0J9gD8beHjTFk1w==} + hasBin: true + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetchdts@0.2.0: + resolution: {integrity: sha512-PJb4ZTGT2/wK78RmVrxZITStWabhZlKBsL52VOgq+c7clEkdHdEB+0rtzrXCbZJqeYJj+IwNWYqB5/G6vouA6A==} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + flru@1.0.2: + resolution: {integrity: sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==} + engines: {node: '>=6'} + + fnv1a-64@0.1.2: + resolution: {integrity: sha512-mJbcILTPybAR1jdOwt/lVv8N2cffeJFFP+gVbSAwLEx1ujXH27RpPd8Rokec/KX9c76UYIB6Co+H4lQzxPnJfA==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + fuzzysort@4.0.2: + resolution: {integrity: sha512-3c7yD6rK1EXIsqpcu1ZVzXJugODWNMzvUij6yTiziaAHrpNS8kzFOI9mJLE8pmG4K9JOaVgzQRgklfOEk8f4Tg==} + + generic-names@4.0.0: + resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.7.0: + resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==} + engines: {node: '>=18'} + + get-port-please@3.2.0: + resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + + get-random-values@3.0.0: + resolution: {integrity: sha512-mNznaBdYcpz7UAdnOtDGcLdNwAa79mXl5htEyyZ51YaeAWNf2g4x/2yCVBdNNTbi35wX0Stc2PJXM7G6rcONOA==} + engines: {node: 18 || >=20} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + giget@3.3.1: + resolution: {integrity: sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==} + hasBin: true + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + + global@4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + + globby@16.2.4: + resolution: {integrity: sha512-c8B/VNLmxRcmqqenRA9t+9IyOjf9+V6lTxPaUJLqOCONdQkWZ0ETYgX0qbtJqPsgCNusT9MZ5Jeidw8Eb9tn2g==} + engines: {node: '>=20'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gzip-size@7.0.0: + resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + h3@1.15.11: + resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + hookable@6.1.2: + resolution: {integrity: sha512-+abwxtiEA52GCVIsQqut3S/uKTbUwYIp4Pe/vv+6py5XiXBCqMZHg6pA6Y5qhgLSEys0/cYuPbO0z1QFj5ZCmg==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-shutdown@1.2.2: + resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + httpxy@0.5.5: + resolution: {integrity: sha512-uDjmnPyp1q4Sgzf3w+J/Fc6UqcCEj0x4Wjp7OqK5dGhNeDgpyrAmnS6ey8QWrX3SWDon2DMKf9sBa5X9+CVyMA==} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@7.0.12: + resolution: {integrity: sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==} + engines: {node: '>= 4'} + + image-meta@0.2.2: + resolution: {integrity: sha512-3MOLanc3sb3LNGWQl1RlQlNWURE5g32aUphrDyFeCsxBTk08iE3VNe4CwsUZ0Qs1X+EfX0+r29Sxdpza4B+yRA==} + + impound@1.2.0: + resolution: {integrity: sha512-hbFh2WURN+XQ364SbblzubhEEj/B3qZCe80l/EHXPWM6/kZP2mvOllxwRBBWNxe4gLbqXZL0fxOZwoDCSlJZQw==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ioredis@5.11.1: + resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} + engines: {node: '>=12.22.0'} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + iron-webcrypto@2.0.0: + resolution: {integrity: sha512-rtffZKDUHciZElM8mjFCufBC7nVhCxHYyWHESqs89OioEDz4parOofd8/uhrejh/INhQFfYQfByS22LlezR9sQ==} + + is-core-module@2.17.0: + resolution: {integrity: sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==} + engines: {node: '>= 0.4'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-installed-globally@1.0.1: + resolution: {integrity: sha512-li88H4QGRoNMX27jEmY8nR515oJVspAgC/WFMlhHfFV8RJ4QyGpHhXnwAe9gH5RIgKmdYRUmrOhQobcgEjIdSw==} + engines: {node: '>=18'} + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} + engines: {node: '>=20'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + klona@2.0.6: + resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} + engines: {node: '>= 8'} + + knitwork@1.3.0: + resolution: {integrity: sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==} + + launch-editor@2.14.2: + resolution: {integrity: sha512-NHJEb30MGBCkmZXLZwz25t37QR4aY03ZzPYUJXOJaOa92uWwExI9SBQUuIznBYc1xkD7cJNGX7oc3Nl3sq2kQQ==} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + listhen@1.10.1: + resolution: {integrity: sha512-6nt/86SkqUQSLW1ofz8MxC6RhRMqOl3ONISe6qqvJ3xj09aJWQx6DhgSZpugs3PX4PXdOas/WD6A9jx6J2N19A==} + hasBin: true + peerDependencies: + '@parcel/watcher': ^2.5.6 + peerDependenciesMeta: + '@parcel/watcher': + optional: true + + loader-utils@3.3.1: + resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==} + engines: {node: '>= 12.13.0'} + + local-pkg@1.2.1: + resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} + engines: {node: '>=14'} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string-ast@1.0.3: + resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==} + engines: {node: '>=20.19.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magic-string@1.4.3: + resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} + + magicast@0.5.5: + resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} + + memory-cache@0.2.0: + resolution: {integrity: sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + microdiff@1.6.0: + resolution: {integrity: sha512-w7JWt8Bno6I8h0rEqlxr4lNG4UbT1FVtWo42wWosIiaJ+rP3pXh7shCYDnyqBrYx36LJ1CZ64p8A62aVp0sJpQ==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@4.1.1: + resolution: {integrity: sha512-YAApRsTpM2+gnUFHhRNGfkX8NK0V5xj1nyQLufGlwEI+eX4xRsTcel2IXDuMEGSGjBvzykNW4VuoEACgwYe7BQ==} + engines: {node: '>=16'} + hasBin: true + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + min-document@2.19.2: + resolution: {integrity: sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + mocked-exports@0.1.1: + resolution: {integrity: sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + my-bad@0.2.11: + resolution: {integrity: sha512-K1R6DRz31BR7YYaDDLRr97XWKWku3SQhHKpDgz7i/VS3O3pvphIzbJIrrEXupSEmPtZElY9UzCrDc0nWvG4HyA==} + engines: {node: '>=22.12.0'} + peerDependencies: + vite: '>=6' + peerDependenciesMeta: + vite: + optional: true + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanotar@0.3.0: + resolution: {integrity: sha512-Kv2JYYiCzt16Kt5QwAc9BFG89xfPNBx+oQL4GQXD9nLqPkZBiNaqaCWtwnbk/q7UVsTYevvM1b0UF8zmEI4pCg==} + + nitropack@2.13.4: + resolution: {integrity: sha512-tX7bT6zxNeMwkc6hxHiZeUoTOjVrcjoh1Z3cmxOlodIqjl4HISgqfGOmkWSayky3Nv9Z5+KQH52F8nmXJY5AAA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + xml2js: ^0.6.2 + peerDependenciesMeta: + xml2js: + optional: true + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-forge@1.4.0: + resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} + engines: {node: '>= 6.13.0'} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-mock-http@1.0.5: + resolution: {integrity: sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==} + + node-releases@2.0.59: + resolution: {integrity: sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==} + engines: {node: '>=18'} + + nopt@8.1.0: + resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nostics@1.3.0: + resolution: {integrity: sha512-wzRsrJwNlFbqIRQt8aCeL2WE8USip/uuvGSOZV1D02CxX8ABTMbw8IywKjH7yWHETMXbWJI4OBpihg9rmz2Vyg==} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + nuxt@4.6.2: + resolution: {integrity: sha512-KfhAIuHQpxKbTN0ItGpkK0jgzjcOCSOf6FZB3wHk/aZ1OiAMsDvbqua1OGfxuEk3bSlK3X49WyFEL5TwEKm36w==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@parcel/watcher': ^2.1.0 + '@types/node': '>=18.12.0' + rolldown: ~1.2.1 + peerDependenciesMeta: + '@parcel/watcher': + optional: true + '@types/node': + optional: true + + object-identity@0.2.3: + resolution: {integrity: sha512-2J8Joz2Tf7aaylhqFvIUJHNgpuGR38Hh75Voq9GzTbStBxJUaOtN0K1aOd3cV5qp+ij1pMqRbPrGCGMOyX303w==} + + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + + obug@3.0.0: + resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} + engines: {node: '>=12.20.0'} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + ohash@2.0.12: + resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==} + + on-change@6.0.2: + resolution: {integrity: sha512-08+12qcOVEA0fS9g/VxKS27HaT94nRutUT77J2dr8zv/unzXopvhBuF8tNLWsoLQ5IgrQ6eptGeGqUYat82U1w==} + engines: {node: '>=20'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + open@11.0.4: + resolution: {integrity: sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==} + engines: {node: '>=20'} + + oxc-walker@1.1.1: + resolution: {integrity: sha512-Hwd7dq28zu/Er99+bpWp16CGwFrhyalJh0W3JOB7XpPvgWo3MqMi2ksWGKuzzA9zt50mJ2pIUwR5lpAdZ9ACNQ==} + peerDependencies: + '@oxc-project/types': '>=0.98.0' + oxc-parser: '>=0.98.0' + rolldown: '>=1.0.0' + peerDependenciesMeta: + '@oxc-project/types': + optional: true + oxc-parser: + optional: true + rolldown: + optional: true + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@1.9.0: + resolution: {integrity: sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.3: + resolution: {integrity: sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==} + + postcss-calc@11.2.3: + resolution: {integrity: sha512-pFW3MR2a4W7uHcSGUiyQGGXZWHon83ljbNwnUJ1mRKublWQbv+3o9NiqhlV4TJfNby+U+zLY2OgiywzhA5eFqw==} + engines: {node: ^22.22.3 || ^24.15 || >=26.0} + peerDependencies: + postcss: ^8.5.29 + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.30: + resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==} + engines: {node: ^10 || ^12 || >=14} + + powershell-utils@0.2.1: + resolution: {integrity: sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==} + engines: {node: '>=20'} + + pretty-bytes@7.2.0: + resolution: {integrity: sha512-T2kroepGB5DRVbkN/nOpW03+VRWqdtJhZNuCW3wYk3chdPjgi4GcTBOaxWfHq1MIM0ypidxsSWbQaAiRpxnXmg==} + engines: {node: '>=20'} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + rc9@3.1.0: + resolution: {integrity: sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.8.0: + resolution: {integrity: sha512-2jhTTU3Eilp/2DMT71gbcoqC35eQHasMJH0RHkYxwlxIv3+FR6Y5xAFZIwEGLxKkjZYEFlsiMNIdwUqjK2lLHQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} + engines: {node: '>= 20.19.0'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve@1.22.13: + resolution: {integrity: sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==} + engines: {node: '>= 0.4'} + hasBin: true + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rolldown-string@0.3.1: + resolution: {integrity: sha512-dv8GOXkYqUQdI0rsXB8hmzO95pKWSuX2eg5/JSJa9czfEVIFuKNR7ZJDfat8LlmD35RAhWY+evS7/wXXqFDfSg==} + engines: {node: '>=20.19.0'} + peerDependencies: + rolldown: '*' + peerDependenciesMeta: + rolldown: + optional: true + + rolldown@1.2.14: + resolution: {integrity: sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup-plugin-visualizer@7.1.1: + resolution: {integrity: sha512-ThaGiHTU8XW02OkK80TrTHATraJmM9OAduU4otal+7gyXLpYEtmGBLfx5kW+EHvvLwn03YGW2NnwKUIqsYlJAA==} + engines: {node: '>=22'} + hasBin: true + peerDependencies: + rolldown: 1.x || ^1.0.0-beta || ^1.0.0-rc + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rolldown: + optional: true + rollup: + optional: true + + rollup@4.64.9: + resolution: {integrity: sha512-mnPSQINChN7PpT+a5CI0/+87hm/+6zxSKBK1XonrzQ0zFnglaTjhkTP2Br4yNCTcLHUEvrAfZffH57jGf+8xBQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rou3@0.9.2: + resolution: {integrity: sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + secure-random-bytes@5.0.1: + resolution: {integrity: sha512-48nvkahmdyoRbHW/x1HTGIk+i2/84MHFBB+rZ50cg1y/p2RjKG+fhTMQKZx4QD6T6/o2M2WHLPkncqQpjMMsUw==} + engines: {node: 18 || >=20} + + secure-random-octet@4.0.1: + resolution: {integrity: sha512-KEeg9wGi/96Q8Edp97gt5Z5dYCcvG7vPKdKFq6rPLCHtaaHwZKbhbC+gY74O7OjX8Fnels49wqc8IgkNhBJZfw==} + engines: {node: 18 || >=20} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serialize-javascript@7.1.2: + resolution: {integrity: sha512-GL2BWwVa6JydKO6l/ljVgjAZF4QJ3S7dWDWi53s5GZT8PJzD2fNxi67HANQGKAqPrwEpL5ba7gUBGi0Ls/sEoQ==} + engines: {node: '>=20.0.0'} + + seroval@1.6.11: + resolution: {integrity: sha512-hpb6XnMCGN/A+79Uf8V7/iTGkyyyazNn5X0CoCAp7Uy9vT3RT8jZ9GLcnxGoVjg3P8AArbe4ZVHq6C8oT2a0kA==} + engines: {node: '>=10'} + + serve-placeholder@2.0.2: + resolution: {integrity: sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.12.0: + resolution: {integrity: sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + smob@1.6.2: + resolution: {integrity: sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==} + engines: {node: '>=20.0.0'} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + source-map@0.8.0: + resolution: {integrity: sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==} + engines: {node: '>= 12'} + + srvx@1.0.5: + resolution: {integrity: sha512-KvSKRpgPG/oaq3cyT614OQ2bAa7DynuGamivCZeoZzIUULOdbQz6wUtnnVuC4+DCx2Zglzo8v5gBYmWIWYhDxA==} + engines: {node: '>=20.16.0'} + hasBin: true + + srvx@1.0.6: + resolution: {integrity: sha512-QeTKgAhk8OSsfeq60bl3wuVs9PmBftzmk7owBInA/F1YQne+TRw0UcPymNpURldAmx8hfmM2PNNGH/zgo3GpLA==} + engines: {node: '>=20.16.0'} + hasBin: true + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@4.3.0: + resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} + + stream-browserify@3.0.0: + resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} + + streamx@2.28.1: + resolution: {integrity: sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.3.0: + resolution: {integrity: sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==} + engines: {node: '>=20'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-literal@4.0.0: + resolution: {integrity: sha512-PaqAvfUZKBwc/SLmNZtHmzK+v19Z4O4eS3cKPeGvbIv/U3pnyEq4Tuw3/4v/FwfM8VQaEawsyCcOQ0P+kpwWWw==} + + structured-clone-es@2.0.1: + resolution: {integrity: sha512-10ZL5r77LhknxlP1FBiCW+VdnuWOEFLdSS2SKtjyEV+L4qP1hUEIMIZW94LC3jKxRmT/Dj7KkD1mqh/IY6lhKQ==} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tar-stream@3.2.2: + resolution: {integrity: sha512-+8NeqHRjQWH9nYlwo2gamAMImZCVzI4UoEgDpWorBt9OEfppiZn+uSkskzQKPWIZyji/C8fpWO7u69G0DX0tbg==} + + tar@7.5.22: + resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} + engines: {node: '>=18'} + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + + terser@5.51.2: + resolution: {integrity: sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==} + engines: {node: '>=10'} + hasBin: true + + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinyclip@0.1.15: + resolution: {integrity: sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==} + engines: {node: ^16.14.0 || >= 17.3.0} + + tinyclip@1.0.3: + resolution: {integrity: sha512-nTv2raKFMzGG6JJp0lpNDsRPdfFE6UHlu2rVEjXo8vkx1ZS3LW2CVXqIaLu8xZlu+6tbFs9V58Qc/Jmk/Ii7wA==} + engines: {node: ^16.14.0 || >= 17.3.0} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-fest@5.10.0: + resolution: {integrity: sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA==} + engines: {node: '>=20'} + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + uint8array-extras@1.6.0: + resolution: {integrity: sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q==} + engines: {node: '>=18'} + + ultrahtml@1.7.0: + resolution: {integrity: sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + unctx@2.5.0: + resolution: {integrity: sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==} + + unctx@3.0.1: + resolution: {integrity: sha512-5RAt2etv7g362RXyd33R82gm9u/kbtQlpoaOs9Bgm4E32GRMJ0xjbZyvdxUTmiuHk3V1IQb1aUNrp5IWY+JaWw==} + peerDependencies: + magic-string: '>=0.30.21' + oxc-parser: '>=0.140.0' + rolldown: ^1.1.5 + unplugin: ^3.3.0 + peerDependenciesMeta: + magic-string: + optional: true + oxc-parser: + optional: true + rolldown: + optional: true + unplugin: + optional: true + + undici@8.12.2: + resolution: {integrity: sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==} + engines: {node: '>=22.19.0'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + unhead@3.4.3: + resolution: {integrity: sha512-0FZI/qCCyIey14CKlGMFpiQumWSatlthJy7Xvro7H3nmi/V2owkcCXG03k0GXDc2/TV1K6bvrbAwhzTRe5Nhdw==} + peerDependencies: + vite: '>=6.4.2' + peerDependenciesMeta: + vite: + optional: true + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + unicorn-magic@0.4.1: + resolution: {integrity: sha512-lzlXPoVB0Uy/FvTaUgX39RbixYiSKoc3JSoSf01+0c2B6FR3qdYIPN7Qjo/AV7n6sqLCk0509cT3LRj1oSZ1+A==} + engines: {node: '>=20'} + + unimport@6.5.0: + resolution: {integrity: sha512-t0KLJLRbebz3f7NrQe+vy2ObeugVeM1XqI2oeYnauddmHSglYj3U6bxOJ65IbXmOFkcAGFJK2OyGH7aqHPM6Ug==} + engines: {node: '>=18.12.0'} + peerDependencies: + oxc-parser: '*' + rolldown: ^1.0.0 + peerDependenciesMeta: + oxc-parser: + optional: true + rolldown: + optional: true + + unimport@7.0.2: + resolution: {integrity: sha512-5yrZs6kHyx5h1ZgeLxK0EgS/JL7Uhwv9HgzQImW8JccGmbYWyXx7Jc4YhU5+sd1Yk31PlfLQhfbZ1oigXipo3A==} + engines: {node: '>=18.12.0'} + peerDependencies: + acorn: ^8.0.0 + oxc-parser: '*' + rolldown: ^1.0.0 + peerDependenciesMeta: + acorn: + optional: true + oxc-parser: + optional: true + rolldown: + optional: true + + unplugin-utils@0.3.2: + resolution: {integrity: sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==} + engines: {node: '>=20.19.0'} + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + unplugin@3.4.0: + resolution: {integrity: sha512-9skdIFlCsPdFV7wUfZxNsFInlW+7nJmGu2gkTu0OUhF56aXGsHab9x52/QhdJ4lC7ZDPWTxbiC4ANqVlsuaW3w==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@farmfe/core': '*' + '@rsbuild/core': '*' + '@rspack/core': '*' + bun-types-no-globals: '*' + esbuild: '*' + rolldown: '*' + rollup: '*' + unloader: '*' + vite: '*' + webpack: '*' + peerDependenciesMeta: + '@farmfe/core': + optional: true + '@rsbuild/core': + optional: true + '@rspack/core': + optional: true + bun-types-no-globals: + optional: true + esbuild: + optional: true + rolldown: + optional: true + rollup: + optional: true + unloader: + optional: true + vite: + optional: true + webpack: + optional: true + + unrouting@0.2.3: + resolution: {integrity: sha512-8wOFT/fBh8YeeDr1aVmkXCqYVTMbQ6vzfNzwzybKmSfXdRhGCq1Y2B1e9YeB0JEf85gbvSwJHAMu3D2IEcLGKA==} + + unstorage@1.17.6: + resolution: {integrity: sha512-gjKtcmn/KwZuJZO9o2sc0esAQnhmTw0LviDHwevTXbJMYs0dRTp8ZEEWXHU0vTpzbN3cA0yxlTeyXssMp3zDqw==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6 || ^7 || ^8 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1 || ^2 || ^3 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + untun@0.2.2: + resolution: {integrity: sha512-+NnOJcSiEtYsVgJmXUzQbJeRAFXJC4yPJYuh6kF9B0Rm6zunXcs/3GZOTllyocSbUDIxD6Bj7e/4ATw7sph1Sw==} + hasBin: true + + untyped@2.0.0: + resolution: {integrity: sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==} + hasBin: true + + unwasm@0.5.3: + resolution: {integrity: sha512-keBgTSfp3r6+s9ZcSma+0chwxQdmLbB5+dAD9vjtB21UTMYuKAxHXCU1K2CbCtnP09EaWeRvACnXk0EJtUx+hw==} + + update-browserslist-db@1.3.4: + resolution: {integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uqr@0.1.3: + resolution: {integrity: sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + + verkit@0.5.0: + resolution: {integrity: sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==} + engines: {node: '>=18.12.0'} + + vite-dev-rpc@2.0.0: + resolution: {integrity: sha512-yKwbTwdHKSD2k/aGqyWpPHepo45OQc8lH3/6IfT4ZqeKE26ooKvi4WIEKzqWav8v+9Is8u1k8q54hvOmqASazA==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 || ^8.0.0 + + vite-hot-client@2.2.0: + resolution: {integrity: sha512-76Zs9zrHbH7M7wqeyooGQKdX+yg0pQ0xuQ1PbFp4z5a0Lzn2e5IPFoCswnmqZ4GiwqB4Jo3WcDAMO9jARTJl8w==} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0 + + vite-node@6.0.0: + resolution: {integrity: sha512-oj4PVrT+pDh6GYf5wfUXkcZyekYS8kKPfLPXVl8qe324Ec6l4K2DUKNadRbZ3LQl0qGcDz+PyOo7ZAh00Y+JjQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + vite-plugin-checker@0.14.5: + resolution: {integrity: sha512-c9lQ92eisUO+F7Fd93aelojmiOS+NQpPgQ1XR2LTQHox1/laZf4yAoQj+L3RA9Vgh10e2nFd9b8r2LLyYZsbpA==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@biomejs/biome': '>=2.4.12' + eslint: '>=9.39.4' + meow: ^13.2.0 || ^14.0.0 + optionator: ^0.9.4 + oxlint: '>=1' + stylelint: '>=16.26.1' + typescript: '*' + vite: '>=5.4.21' + vue-tsc: ~2.2.10 || ^3.0.0 + peerDependenciesMeta: + '@biomejs/biome': + optional: true + eslint: + optional: true + meow: + optional: true + optionator: + optional: true + oxlint: + optional: true + stylelint: + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + vite-plugin-inspect@11.4.1: + resolution: {integrity: sha512-ShOFe2PURXGvRS5OrgmOLZOCwDTD7dEBVt0tMpFPKb9AsvqXKCRGM8QgKrUbRbJYFXScHvDPpGRd28rYidC0tA==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^6.0.0 || ^7.0.0-0 || ^8.0.0-0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + + vite-plugin-vue-tracer@1.6.0: + resolution: {integrity: sha512-G07az+X68rtz9ngfBxZH8vVhZ7DOnZvJABnT9nFp/TBV+qfKtGqA00djblqvvbf3VYfwLmJtoqZhFDAx8mLpNw==} + peerDependencies: + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vue: ^3.5.0 + + vite@8.3.4: + resolution: {integrity: sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vue-bundle-renderer@2.4.0: + resolution: {integrity: sha512-IxHopQZgzTGBiuGERrQrAS7Lr3dCv+qxvgirEgCUFGAD4gYNfiaGYzC1DDojeZmJAmehp9gG8+17PGuVP6Ab2Q==} + + vue-component-type-helpers@3.3.13: + resolution: {integrity: sha512-02KCxSmKDhIITjKyJ4QT5csP6L0WoRborLqY/34a40+S9uvMvdNgMV0yVjO3p2Gkz3ZoPmFz/0JsGHacwJYTxw==} + + vue-devtools-stub@0.1.0: + resolution: {integrity: sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==} + + vue-router@5.4.0: + resolution: {integrity: sha512-pFvcYcxinDHjZUlf7AzFBrT8r8Xqvv4uMHoJnjc+EqPawXQvHTkF8S4tfSmQLpriVHUGfkaKlI7Et0KRjTMTdw==} + peerDependencies: + '@pinia/colada': '>=0.21.2' + '@vue/compiler-sfc': ^3.5.34 || ^4.0.0 + pinia: ^3.0.4 || ^4.0.2 + vite: ^7.3.0 || ^8.0.0 + vue: ^3.5.34 || ^4.0.0 + peerDependenciesMeta: + '@pinia/colada': + optional: true + '@vue/compiler-sfc': + optional: true + pinia: + optional: true + vite: + optional: true + + vue@3.5.44: + resolution: {integrity: sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@6.0.1: + resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + ws@8.22.0: + resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@1.0.1: + resolution: {integrity: sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==} + engines: {node: '>=20'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.2.0: + resolution: {integrity: sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.1: + resolution: {integrity: sha512-mxW3qiSnl+GRxXsaUMzv2Mbada1Y8CDltET9UxejDQe6DBYlSekghl5U5K0ReAikcHDi0G1vKZEmmo/NWAGKLA==} + + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.10 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.29.7': + dependencies: + '@babel/types': 7.29.8 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.29.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/traverse': 7.29.10 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-member-expression-to-functions@7.29.7': + dependencies: + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.29.7': + dependencies: + '@babel/types': 7.29.8 + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/traverse': 7.29.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + dependencies: + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-string-parser@8.0.6': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-identifier@8.0.6': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.10': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + + '@babel/parser@8.0.7': + dependencies: + '@babel/types': 8.0.6 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-typescript@7.29.9(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.10': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@babel/types@8.0.6': + dependencies: + '@babel/helper-string-parser': 8.0.6 + '@babel/helper-validator-identifier': 8.0.6 + + '@clack/core@1.5.1': + dependencies: + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@clack/prompts@1.8.1': + dependencies: + '@clack/core': 1.5.1 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@cloudflare/kv-asset-handler@0.4.2': {} + + '@colordx/core@7.3.0': {} + + '@csstools/css-tokenizer@4.0.2': {} + + '@dxup/nuxt@0.5.10(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(jiti@2.7.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))': + dependencies: + '@dxup/unimport': 0.1.2 + '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))) + '@vue/compiler-dom': 3.5.44 + chokidar: 5.0.0 + knitwork: 1.3.0 + magic-string: 1.4.3 + pathe: 2.0.3 + tinyglobby: 0.2.17 + unplugin: 3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + transitivePeerDependencies: + - '@farmfe/core' + - '@nuxt/schema' + - '@rsbuild/core' + - '@rspack/core' + - bun-types-no-globals + - confbox + - dotenv + - esbuild + - giget + - jiti + - mlly + - oxc-parser + - rolldown + - rollup + - unloader + - vite + - webpack + + '@dxup/unimport@0.1.2': {} + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@ioredis/commands@1.10.0': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@mapbox/node-pre-gyp@2.0.3': + dependencies: + consola: 3.4.2 + detect-libc: 2.1.2 + https-proxy-agent: 7.0.6 + node-fetch: 2.7.0 + nopt: 8.1.0 + semver: 7.8.5 + tar: 7.5.22 + transitivePeerDependencies: + - encoding + - supports-color + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.3 + + '@nuxt/cli@4.0.1(@nuxt/schema@4.6.2)(jiti@2.7.0)(rolldown@1.2.14)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))': + dependencies: + '@clack/prompts': 1.8.1 + args-tokenizer: 0.3.0 + clickable-path: 0.1.1 + confbox: 0.3.1 + consola: 3.4.2 + defu: 6.1.7 + exsolve: 1.1.3 + fuzzysort: 4.0.2 + get-port-please: 3.2.0 + my-bad: 0.2.11(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + obug: 3.0.0 + package-manager-detector: 1.9.0 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.3 + rc9: 3.1.0 + scule: 1.3.0 + srvx: 1.0.6 + std-env: 4.3.0 + tinyclip: 1.0.3 + tinyexec: 1.3.1 + uqr: 0.1.3 + verkit: 0.5.0 + optionalDependencies: + '@nuxt/schema': 4.6.2 + jiti: 2.7.0 + rolldown: 1.2.14 + transitivePeerDependencies: + - vite + + '@nuxt/devalue@2.0.2': {} + + '@nuxt/devtools-kit@3.4.3(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))': + dependencies: + '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))) + execa: 8.0.1 + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + transitivePeerDependencies: + - '@nuxt/schema' + - confbox + - dotenv + - giget + - jiti + - magic-string + - mlly + - oxc-parser + - rolldown + - unplugin + + '@nuxt/devtools-wizard@3.4.3': + dependencies: + '@clack/prompts': 1.8.1 + consola: 3.4.2 + diff: 8.0.4 + execa: 8.0.1 + magicast: 0.5.5 + pathe: 2.0.3 + pkg-types: 2.3.3 + semver: 7.8.5 + + '@nuxt/devtools@3.4.3(@nuxt/schema@4.6.2)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44)': + dependencies: + '@nuxt/devtools-kit': 3.4.3(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + '@nuxt/devtools-wizard': 3.4.3 + '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))) + '@vue/devtools-core': 8.2.1(vue@3.5.44) + '@vue/devtools-kit': 8.2.1 + birpc: 4.2.0 + consola: 3.4.2 + destr: 2.0.5 + error-stack-parser-es: 2.0.1 + execa: 8.0.1 + fast-npm-meta: 2.2.0 + get-port-please: 3.2.0 + hookable: 6.1.2 + image-meta: 0.2.2 + is-installed-globally: 1.0.1 + launch-editor: 2.14.2 + local-pkg: 1.2.1 + magicast: 0.5.5 + ohash: 2.0.12 + package-manager-detector: 1.9.0 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.3 + semver: 7.8.5 + sirv: 3.0.2 + structured-clone-es: 2.0.1 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + unstorage: 1.17.6(db0@0.3.4)(ioredis@5.11.1) + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + vite-plugin-inspect: 11.4.1(@nuxt/kit@4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))))(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + vite-plugin-vue-tracer: 1.6.0(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44) + which: 6.0.1 + ws: 8.22.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@nuxt/schema' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - confbox + - db0 + - dotenv + - giget + - idb-keyval + - ioredis + - jiti + - magic-string + - mlly + - oxc-parser + - rolldown + - unplugin + - uploadthing + - utf-8-validate + - vue + + '@nuxt/kit@3.21.11(magicast@0.5.5)': + dependencies: + c12: 3.3.4(magicast@0.5.5) + consola: 3.4.2 + defu: 6.1.7 + destr: 2.0.5 + errx: 0.1.2 + exsolve: 1.1.3 + ignore: 7.0.12 + jiti: 2.7.0 + klona: 2.0.6 + knitwork: 1.3.0 + mlly: 1.8.2 + ohash: 2.0.12 + pathe: 2.0.3 + pkg-types: 2.3.3 + rc9: 3.1.0 + scule: 1.3.0 + semver: 7.8.5 + tinyglobby: 0.2.17 + ufo: 1.6.4 + unctx: 2.5.0 + untyped: 2.0.0 + transitivePeerDependencies: + - magicast + + '@nuxt/kit@4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))': + dependencies: + consola: 3.4.2 + defu: 6.1.7 + destr: 2.0.5 + errx: 0.2.2 + exsolve: 1.1.3 + ignore: 7.0.12 + klona: 2.0.6 + microdiff: 1.6.0 + nostics: 1.3.0 + package-manager-detector: 1.9.0 + pathe: 2.0.3 + rc9: 3.1.0 + scule: 1.3.0 + std-env: 4.3.0 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + unctx: 3.0.1(magic-string@1.4.3)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))) + verkit: 0.5.0 + optionalDependencies: + '@nuxt/schema': 4.6.2 + confbox: 0.3.1 + dotenv: 17.4.2 + giget: 3.3.1 + jiti: 2.7.0 + mlly: 1.8.2 + rolldown: 1.2.14 + transitivePeerDependencies: + - magic-string + - unplugin + + '@nuxt/nitro-server@4.6.2(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@nuxt/schema@4.6.2)(@oxc-project/types@0.154.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.33.0)(magic-string@1.4.3)(mlly@1.8.2)(nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.154.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.9))(rollup@4.64.9)(srvx@1.0.6)(terser@5.51.2)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(rolldown@1.2.14)(rollup@4.64.9)(srvx@1.0.6)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))': + dependencies: + '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))) + '@unhead/vue': 3.4.3(@oxc-project/types@0.154.0)(esbuild@0.28.2)(lightningcss@1.33.0)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44) + consola: 3.4.2 + defu: 6.1.7 + destr: 2.0.5 + devalue: 6.0.3 + errx: 0.2.2 + escape-string-regexp: 5.0.0 + exsolve: 1.1.3 + flru: 1.0.2 + h3: 1.15.11 + impound: 1.2.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + klona: 2.0.6 + mocked-exports: 0.1.1 + my-bad: 0.2.11(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + nitropack: 2.13.4(rolldown@1.2.14)(srvx@1.0.6)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + nostics: 1.3.0 + nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.154.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.9))(rollup@4.64.9)(srvx@1.0.6)(terser@5.51.2)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + ohash: 2.0.12 + pathe: 2.0.3 + radix3: 1.1.2 + rou3: 0.9.2 + std-env: 4.3.0 + ufo: 1.6.4 + unctx: 3.0.1(magic-string@1.4.3)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))) + unrouting: 0.2.3 + unstorage: 1.17.6(db0@0.3.4)(ioredis@5.11.1) + vue: 3.5.44 + vue-bundle-renderer: 2.4.0 + vue-devtools-stub: 0.1.0 + optionalDependencies: + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@farmfe/core' + - '@libsql/client' + - '@netlify/blobs' + - '@nuxt/schema' + - '@oxc-project/types' + - '@parcel/watcher' + - '@planetscale/database' + - '@rsbuild/core' + - '@rspack/core' + - '@unhead/cli' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - '@vitejs/devtools-kit' + - aws4fetch + - bare-abort-controller + - bare-buffer + - better-sqlite3 + - bun-types-no-globals + - confbox + - db0 + - dotenv + - drizzle-orm + - encoding + - esbuild + - giget + - idb-keyval + - ioredis + - jiti + - lightningcss + - magic-string + - mlly + - mysql2 + - oxc-parser + - react-native-b4a + - rolldown + - rollup + - sqlite3 + - srvx + - supports-color + - typescript + - unloader + - unplugin + - uploadthing + - vite + - webpack + - xml2js + + '@nuxt/schema@4.6.2': + dependencies: + '@vue/shared': 3.5.44 + defu: 6.1.7 + nostics: 1.3.0 + pathe: 2.0.3 + pkg-types: 2.3.3 + std-env: 4.3.0 + + '@nuxt/telemetry@2.9.1(@nuxt/kit@4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))))': + dependencies: + '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))) + citty: 0.2.2 + consola: 3.4.2 + rc9: 3.1.0 + std-env: 4.3.0 + + '@nuxt/vite-builder@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.154.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.9))(rollup@4.64.9)(srvx@1.0.6)(terser@5.51.2)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.9))(terser@5.51.2)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(vue@3.5.44)': + dependencies: + '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))) + '@vitejs/plugin-vue': 6.0.10(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44) + '@vitejs/plugin-vue-jsx': 5.1.6(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44) + autoprefixer: 10.6.1(postcss@8.5.30) + clickable-path: 0.1.1 + consola: 3.4.2 + cssnano: 9.9.0(postcss@8.5.30) + defu: 6.1.7 + escape-string-regexp: 5.0.0 + exsolve: 1.1.3 + generic-names: 4.0.0 + get-port-please: 3.2.0 + js-tokens: 10.0.0 + knitwork: 1.3.0 + mocked-exports: 0.1.1 + my-bad: 0.2.11(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.154.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.9))(rollup@4.64.9)(srvx@1.0.6)(terser@5.51.2)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + pathe: 2.0.3 + pkg-types: 2.3.3 + postcss: 8.5.30 + rolldown-string: 0.3.1(rolldown@1.2.14) + seroval: 1.6.11 + std-env: 4.3.0 + ufo: 1.6.4 + unenv: 2.0.0-rc.24 + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + vite-node: 6.0.0(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + vite-plugin-checker: 0.14.5(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + vue: 3.5.44 + vue-bundle-renderer: 2.4.0 + optionalDependencies: + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + rolldown: 1.2.14 + rollup-plugin-visualizer: 7.1.1(rolldown@1.2.14)(rollup@4.64.9) + transitivePeerDependencies: + - '@biomejs/biome' + - '@nuxt/schema' + - '@types/node' + - '@vitejs/devtools' + - confbox + - dotenv + - esbuild + - eslint + - giget + - jiti + - less + - magic-string + - meow + - mlly + - optionator + - oxc-parser + - oxlint + - sass + - sass-embedded + - stylelint + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - unplugin + - vue-tsc + - yaml + + '@nuxt/vite-server@4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.154.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.9))(rollup@4.64.9)(srvx@1.0.6)(terser@5.51.2)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))': + dependencies: + '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))) + clickable-path: 0.1.1 + defu: 6.1.7 + exsolve: 1.1.3 + mocked-exports: 0.1.1 + my-bad: 0.2.11(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.154.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.9))(rollup@4.64.9)(srvx@1.0.6)(terser@5.51.2)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + pathe: 2.0.3 + rou3: 0.9.2 + srvx: 1.0.5 + ufo: 1.6.4 + ultrahtml: 1.7.0 + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + transitivePeerDependencies: + - '@nuxt/schema' + - confbox + - dotenv + - giget + - jiti + - magic-string + - mlly + - oxc-parser + - rolldown + - unplugin + + '@oxc-project/types@0.154.0': {} + + '@parcel/watcher-wasm@2.6.0': + dependencies: + is-glob: 4.0.3 + picomatch: 4.0.7 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@polka/url@1.0.0-next.29': {} + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.7.0': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + + '@rolldown/binding-android-arm-eabi@1.2.14': + optional: true + + '@rolldown/binding-android-arm64@1.2.14': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.14': + optional: true + + '@rolldown/binding-darwin-x64@1.2.14': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.14': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.14': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.14': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.14': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.14': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.14': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.14': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.14': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.14': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.14': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.14': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@rollup/plugin-alias@6.0.0(rollup@4.64.9)': + optionalDependencies: + rollup: 4.64.9 + + '@rollup/plugin-commonjs@29.0.3(rollup@4.64.9)': + dependencies: + '@rollup/pluginutils': 5.4.0(rollup@4.64.9) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.5.0(picomatch@4.0.7) + is-reference: 1.2.1 + magic-string: 0.30.21 + picomatch: 4.0.7 + optionalDependencies: + rollup: 4.64.9 + + '@rollup/plugin-inject@5.0.5(rollup@4.64.9)': + dependencies: + '@rollup/pluginutils': 5.4.0(rollup@4.64.9) + estree-walker: 2.0.2 + magic-string: 0.30.21 + optionalDependencies: + rollup: 4.64.9 + + '@rollup/plugin-json@6.1.0(rollup@4.64.9)': + dependencies: + '@rollup/pluginutils': 5.4.0(rollup@4.64.9) + optionalDependencies: + rollup: 4.64.9 + + '@rollup/plugin-node-resolve@16.0.3(rollup@4.64.9)': + dependencies: + '@rollup/pluginutils': 5.4.0(rollup@4.64.9) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.13 + optionalDependencies: + rollup: 4.64.9 + + '@rollup/plugin-replace@6.0.3(rollup@4.64.9)': + dependencies: + '@rollup/pluginutils': 5.4.0(rollup@4.64.9) + magic-string: 0.30.21 + optionalDependencies: + rollup: 4.64.9 + + '@rollup/plugin-terser@1.0.0(rollup@4.64.9)': + dependencies: + serialize-javascript: 7.1.2 + smob: 1.6.2 + terser: 5.51.2 + optionalDependencies: + rollup: 4.64.9 + + '@rollup/pluginutils@5.4.0(rollup@4.64.9)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.7 + optionalDependencies: + rollup: 4.64.9 + + '@rollup/rollup-android-arm-eabi@4.64.9': + optional: true + + '@rollup/rollup-android-arm64@4.64.9': + optional: true + + '@rollup/rollup-darwin-arm64@4.64.9': + optional: true + + '@rollup/rollup-darwin-x64@4.64.9': + optional: true + + '@rollup/rollup-freebsd-arm64@4.64.9': + optional: true + + '@rollup/rollup-freebsd-x64@4.64.9': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.64.9': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.64.9': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.64.9': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.64.9': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.64.9': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.64.9': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-x64-musl@4.64.9': + optional: true + + '@rollup/rollup-openbsd-x64@4.64.9': + optional: true + + '@rollup/rollup-openharmony-arm64@4.64.9': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.64.9': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.64.9': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.64.9': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.64.9': + optional: true + + '@sindresorhus/is@7.2.0': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@speed-highlight/core@1.2.24': {} + + '@thunderid/browser@1.3.0': + dependencies: + '@thunderid/javascript': 1.4.0 + base64url: 3.0.1 + buffer: 6.0.3 + core-js: 3.42.0 + fast-sha256: 1.3.0 + jose: 6.2.3 + process: 0.11.10 + randombytes: 2.1.0 + stream-browserify: 3.0.0 + tslib: 2.8.1 + + '@thunderid/javascript@1.4.0': + dependencies: + jose: 6.2.3 + tslib: 2.8.1 + + '@thunderid/node@1.3.0': + dependencies: + '@thunderid/javascript': 1.4.0 + base64url: 3.0.1 + cross-fetch: 4.1.0 + fast-sha256: 1.3.0 + jose: 6.2.3 + memory-cache: 0.2.0 + secure-random-bytes: 5.0.1 + tslib: 2.8.1 + uuid: 11.1.1 + transitivePeerDependencies: + - encoding + + '@thunderid/nuxt@1.3.0(magicast@0.5.5)(nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.154.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.9))(rollup@4.64.9)(srvx@1.0.6)(terser@5.51.2)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(vue-router@5.4.0(@vue/compiler-sfc@3.5.44)(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44))(vue@3.5.44)': + dependencies: + '@nuxt/kit': 3.21.11(magicast@0.5.5) + '@thunderid/browser': 1.3.0 + '@thunderid/node': 1.3.0 + '@thunderid/vue': 1.4.0(vue-router@5.4.0(@vue/compiler-sfc@3.5.44)(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44))(vue@3.5.44) + defu: 6.1.5 + jose: 6.2.3 + nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.154.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.9))(rollup@4.64.9)(srvx@1.0.6)(terser@5.51.2)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + vue: 3.5.44 + transitivePeerDependencies: + - encoding + - magicast + - vue-router + + '@thunderid/vue@1.4.0(vue-router@5.4.0(@vue/compiler-sfc@3.5.44)(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44))(vue@3.5.44)': + dependencies: + '@thunderid/browser': 1.3.0 + dompurify: 3.4.13 + tslib: 2.8.1 + vue: 3.5.44 + optionalDependencies: + vue-router: 5.4.0(@vue/compiler-sfc@3.5.44)(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44) + + '@types/estree@1.0.9': {} + + '@types/resolve@1.20.2': {} + + '@types/trusted-types@2.0.7': + optional: true + + '@unhead/bundler@3.4.3(@oxc-project/types@0.154.0)(esbuild@0.28.2)(lightningcss@1.33.0)(rolldown@1.2.14)(rollup@4.64.9)(unhead@3.4.3(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))': + dependencies: + magic-string: 1.4.3 + oxc-walker: 1.1.1(@oxc-project/types@0.154.0)(rolldown@1.2.14) + unhead: 3.4.3(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + unplugin: 3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + optionalDependencies: + esbuild: 0.28.2 + lightningcss: 1.33.0 + rolldown: 1.2.14 + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + transitivePeerDependencies: + - '@farmfe/core' + - '@oxc-project/types' + - '@rsbuild/core' + - '@rspack/core' + - bun-types-no-globals + - rollup + - unloader + + '@unhead/vue@3.4.3(@oxc-project/types@0.154.0)(esbuild@0.28.2)(lightningcss@1.33.0)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44)': + dependencies: + '@unhead/bundler': 3.4.3(@oxc-project/types@0.154.0)(esbuild@0.28.2)(lightningcss@1.33.0)(rolldown@1.2.14)(rollup@4.64.9)(unhead@3.4.3(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + hookable: 6.1.2 + unhead: 3.4.3(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + unplugin: 3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + vue: 3.5.44 + optionalDependencies: + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + transitivePeerDependencies: + - '@farmfe/core' + - '@oxc-project/types' + - '@rsbuild/core' + - '@rspack/core' + - '@unhead/cli' + - '@vitejs/devtools-kit' + - bun-types-no-globals + - esbuild + - lightningcss + - oxc-parser + - rolldown + - rollup + - unloader + + '@vercel/nft@1.11.0(rollup@4.64.9)': + dependencies: + '@mapbox/node-pre-gyp': 2.0.3 + '@rollup/pluginutils': 5.4.0(rollup@4.64.9) + acorn: 8.19.0 + acorn-import-attributes: 1.9.5(acorn@8.19.0) + async-sema: 3.1.1 + bindings: 1.5.0 + estree-walker: 2.0.2 + glob: 13.0.6 + graceful-fs: 4.2.11 + node-gyp-build: 4.8.4 + picomatch: 4.0.7 + resolve-from: 5.0.0 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@vitejs/plugin-vue-jsx@5.1.6(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44)': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-typescript': 7.29.9(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.1 + '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.7) + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + vue: 3.5.44 + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-vue@6.0.10(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44)': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + vue: 3.5.44 + + '@vue-macros/common@3.1.4(vue@3.5.44)': + dependencies: + '@vue/compiler-sfc': 3.5.44 + ast-kit: 2.2.0 + local-pkg: 1.2.1 + magic-string-ast: 1.0.3 + unplugin-utils: 0.3.2 + optionalDependencies: + vue: 3.5.44 + + '@vue/babel-helper-vue-transform-on@2.0.1': {} + + '@vue/babel-plugin-jsx@2.0.1(@babel/core@7.29.7)': + dependencies: + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 + '@vue/babel-helper-vue-transform-on': 2.0.1 + '@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.29.7) + '@vue/shared': 3.5.44 + optionalDependencies: + '@babel/core': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@2.0.1(@babel/core@7.29.7)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/parser': 7.29.9 + '@vue/compiler-sfc': 3.5.44 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.44': + dependencies: + '@babel/parser': 7.29.9 + '@vue/shared': 3.5.44 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.2 + + '@vue/compiler-dom@3.5.44': + dependencies: + '@vue/compiler-core': 3.5.44 + '@vue/shared': 3.5.44 + + '@vue/compiler-sfc@3.5.44': + dependencies: + '@babel/parser': 7.29.9 + '@vue/compiler-core': 3.5.44 + '@vue/compiler-dom': 3.5.44 + '@vue/compiler-ssr': 3.5.44 + '@vue/shared': 3.5.44 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.30 + source-map-js: 1.2.2 + + '@vue/compiler-ssr@3.5.44': + dependencies: + '@vue/compiler-dom': 3.5.44 + '@vue/shared': 3.5.44 + + '@vue/devtools-api@8.2.1': + dependencies: + '@vue/devtools-kit': 8.2.1 + + '@vue/devtools-core@8.2.1(vue@3.5.44)': + dependencies: + '@vue/devtools-kit': 8.2.1 + '@vue/devtools-shared': 8.2.1 + vue: 3.5.44 + + '@vue/devtools-kit@8.2.1': + dependencies: + '@vue/devtools-shared': 8.2.1 + birpc: 2.9.0 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + + '@vue/devtools-shared@8.2.1': {} + + '@vue/reactivity@3.5.44': + dependencies: + '@vue/shared': 3.5.44 + + '@vue/runtime-core@3.5.44': + dependencies: + '@vue/reactivity': 3.5.44 + '@vue/shared': 3.5.44 + + '@vue/runtime-dom@3.5.44': + dependencies: + '@vue/reactivity': 3.5.44 + '@vue/runtime-core': 3.5.44 + '@vue/shared': 3.5.44 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.44': + dependencies: + '@vue/compiler-ssr': 3.5.44 + '@vue/runtime-dom': 3.5.44 + '@vue/shared': 3.5.44 + + '@vue/shared@3.5.44': {} + + abbrev@3.0.1: {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + acorn-import-attributes@1.9.5(acorn@8.19.0): + dependencies: + acorn: 8.19.0 + + acorn@8.19.0: {} + + agent-base@7.1.4: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.4.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + ansis@4.4.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + archiver-utils@5.0.2: + dependencies: + glob: 10.5.0 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.18.1 + normalize-path: 3.0.0 + readable-stream: 4.8.0 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.8.0 + readdir-glob: 1.1.3 + tar-stream: 3.2.2 + zip-stream: 6.0.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + args-tokenizer@0.3.0: {} + + ast-kit@2.2.0: + dependencies: + '@babel/parser': 7.29.9 + pathe: 2.0.3 + + ast-kit@3.0.0: + dependencies: + '@babel/parser': 8.0.7 + estree-walker: 3.0.3 + pathe: 2.0.3 + + ast-walker-scope@0.10.0: + dependencies: + '@babel/parser': 8.0.7 + '@babel/types': 8.0.6 + ast-kit: 3.0.0 + + async-sema@3.1.1: {} + + async@3.2.6: {} + + autoprefixer@10.6.1(postcss@8.5.30): + dependencies: + browserslist: 4.29.3 + caniuse-lite: 1.0.30001816 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.30 + postcss-value-parser: 4.2.0 + + b4a@1.9.0: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + bare-events@2.9.2: {} + + bare-fs@4.8.4: + dependencies: + bare-events: 2.9.2 + bare-path: 3.1.2 + bare-stream: 2.13.4(bare-events@2.9.2) + bare-url: 2.5.5 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-path@3.1.2: {} + + bare-stream@2.13.4(bare-events@2.9.2): + dependencies: + b4a: 1.9.0 + streamx: 2.28.1 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.9.2 + transitivePeerDependencies: + - react-native-b4a + + bare-url@2.5.5: + dependencies: + bare-path: 3.1.2 + + base64-js@1.5.1: {} + + base64url@3.0.1: {} + + baseline-browser-mapping@2.11.29: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + birpc@2.9.0: {} + + birpc@4.2.0: {} + + brace-expansion@2.1.7: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.29.3: + dependencies: + baseline-browser-mapping: 2.11.29 + caniuse-lite: 1.0.30001816 + electron-to-chromium: 1.5.459 + node-releases: 2.0.59 + update-browserslist-db: 1.3.4(browserslist@4.29.3) + + buffer-crc32@1.0.0: {} + + buffer-from@1.1.2: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bundle-name@4.1.1: + dependencies: + run-applescript: 7.1.0 + + c12@3.3.4(magicast@0.5.5): + dependencies: + chokidar: 5.0.0 + confbox: 0.2.4 + defu: 6.1.7 + dotenv: 17.4.2 + exsolve: 1.1.3 + giget: 3.3.1 + jiti: 2.7.0 + ohash: 2.0.12 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.3 + rc9: 3.1.0 + optionalDependencies: + magicast: 0.5.5 + + cac@7.1.0: {} + + caniuse-api@4.0.0: + dependencies: + browserslist: 4.29.3 + caniuse-lite: 1.0.30001816 + + caniuse-lite@1.0.30001816: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.1.1 + + chownr@3.0.0: {} + + citty@0.1.6: + dependencies: + consola: 3.4.2 + + citty@0.2.2: {} + + clickable-path@0.1.1: {} + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + cluster-key-slot@1.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@2.20.3: {} + + commondir@1.0.1: {} + + compatx@0.2.0: {} + + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.8.0 + + confbox@0.1.8: {} + + confbox@0.2.4: {} + + confbox@0.3.1: {} + + consola@3.4.2: {} + + convert-source-map@2.0.0: {} + + cookie-es@1.2.3: {} + + cookie-es@2.0.1: {} + + cookie-es@3.1.1: {} + + core-js@3.42.0: {} + + core-util-is@1.0.3: {} + + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.8.0 + + croner@10.0.1: {} + + cross-fetch@4.1.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + + crossws@0.4.15(srvx@1.0.6): + optionalDependencies: + srvx: 1.0.6 + + cssnano@9.9.0(postcss@8.5.30): + dependencies: + '@colordx/core': 7.3.0 + '@csstools/css-tokenizer': 4.0.2 + browserslist: 4.29.3 + caniuse-api: 4.0.0 + postcss: 8.5.30 + postcss-calc: 11.2.3(postcss@8.5.30) + + csstype@3.2.3: {} + + db0@0.3.4: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deepmerge@4.3.1: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.1: + dependencies: + bundle-name: 4.1.1 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + defu@6.1.5: {} + + defu@6.1.7: {} + + denque@2.1.0: {} + + depd@2.0.0: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + + devalue@6.0.3: {} + + diff@8.0.4: {} + + dom-walk@0.1.2: {} + + dompurify@3.4.13: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + dot-prop@10.2.0: + dependencies: + type-fest: 5.10.0 + + dotenv@17.4.2: {} + + duplexer@0.1.2: {} + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.459: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + encodeurl@2.0.0: {} + + entities@7.0.1: {} + + error-stack-parser-es@1.0.5: {} + + error-stack-parser-es@2.0.1: {} + + errx@0.1.2: {} + + errx@0.2.2: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.3.2: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@5.0.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + events-universal@1.0.1: + dependencies: + bare-events: 2.9.2 + transitivePeerDependencies: + - bare-abort-controller + + events@3.3.0: {} + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + exsolve@1.1.3: {} + + fast-fifo@1.3.2: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-npm-meta@2.2.0: + dependencies: + cac: 7.1.0 + + fast-sha256@1.3.0: {} + + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + + fastq@1.20.3: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fetchdts@0.2.0: {} + + file-uri-to-path@1.0.0: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + flru@1.0.2: {} + + fnv1a-64@0.1.2: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fraction.js@5.3.4: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + fuzzysort@4.0.2: {} + + generic-names@4.0.0: + dependencies: + loader-utils: 3.3.1 + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.7.0: {} + + get-port-please@3.2.0: {} + + get-random-values@3.0.0: + dependencies: + global: 4.4.0 + + get-stream@8.0.1: {} + + giget@3.3.1: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@13.0.6: + dependencies: + minimatch: 10.2.6 + minipass: 7.1.3 + path-scurry: 2.0.2 + + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + + global@4.4.0: + dependencies: + min-document: 2.19.2 + process: 0.11.10 + + globby@16.2.4: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + fast-glob: 3.3.3 + ignore: 7.0.12 + is-path-inside: 4.0.0 + micromatch: 4.0.8 + slash: 5.1.0 + unicorn-magic: 0.4.1 + + graceful-fs@4.2.11: {} + + gzip-size@7.0.0: + dependencies: + duplexer: 0.1.2 + + h3@1.15.11: + dependencies: + cookie-es: 1.2.3 + crossws: 0.3.5 + defu: 6.1.7 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.5 + radix3: 1.1.2 + ufo: 1.6.4 + uncrypto: 0.1.3 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hookable@5.5.3: {} + + hookable@6.1.2: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-shutdown@1.2.2: {} + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + httpxy@0.5.5: {} + + human-signals@5.0.0: {} + + ieee754@1.2.1: {} + + ignore@7.0.12: {} + + image-meta@0.2.2: {} + + impound@1.2.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + es-module-lexer: 2.3.2 + pathe: 2.0.3 + unplugin: 3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + unplugin-utils: 0.3.2 + transitivePeerDependencies: + - '@farmfe/core' + - '@rsbuild/core' + - '@rspack/core' + - bun-types-no-globals + - esbuild + - rolldown + - rollup + - unloader + - vite + - webpack + + inherits@2.0.4: {} + + ini@4.1.1: {} + + ioredis@5.11.1: + dependencies: + '@ioredis/commands': 1.10.0 + cluster-key-slot: 1.1.1 + debug: 4.4.3 + denque: 2.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + iron-webcrypto@1.2.1: {} + + iron-webcrypto@2.0.0: + dependencies: + uint8array-extras: 1.6.0 + + is-core-module@2.17.0: + dependencies: + hasown: 2.0.4 + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-installed-globally@1.0.1: + dependencies: + global-directory: 4.0.1 + is-path-inside: 4.0.0 + + is-module@1.0.0: {} + + is-number@7.0.0: {} + + is-path-inside@4.0.0: {} + + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.9 + + is-stream@2.0.1: {} + + is-stream@3.0.0: {} + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isarray@1.0.0: {} + + isexe@2.0.0: {} + + isexe@4.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@2.7.0: {} + + jose@6.2.3: {} + + js-tokens@10.0.0: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + kleur@4.1.5: {} + + klona@2.0.6: {} + + knitwork@1.3.0: {} + + launch-editor@2.14.2: + dependencies: + picocolors: 1.1.1 + shell-quote: 1.12.0 + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + listhen@1.10.1(srvx@1.0.6): + dependencies: + '@parcel/watcher-wasm': 2.6.0 + citty: 0.2.2 + consola: 3.4.2 + crossws: 0.4.15(srvx@1.0.6) + defu: 6.1.7 + get-port-please: 3.2.0 + h3: 1.15.11 + http-shutdown: 1.2.2 + jiti: 2.7.0 + node-forge: 1.4.0 + pathe: 2.0.3 + std-env: 4.3.0 + tinyclip: 0.1.15 + ufo: 1.6.4 + untun: 0.2.2 + uqr: 0.1.3 + transitivePeerDependencies: + - srvx + + loader-utils@3.3.1: {} + + local-pkg@1.2.1: + dependencies: + mlly: 1.8.2 + pkg-types: 2.3.3 + quansync: 0.2.11 + + lodash@4.18.1: {} + + lru-cache@10.4.3: {} + + lru-cache@11.5.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string-ast@1.0.3: + dependencies: + magic-string: 0.30.21 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + magic-string@1.4.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + magicast@0.5.5: + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + source-map-js: 1.2.2 + + memory-cache@0.2.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + microdiff@1.6.0: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@4.1.1: {} + + mimic-fn@4.0.0: {} + + min-document@2.19.2: + dependencies: + dom-walk: 0.1.2 + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.7 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.7 + + minipass@7.1.3: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + + mlly@1.8.2: + dependencies: + acorn: 8.19.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + mocked-exports@0.1.1: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + my-bad@0.2.11(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)): + dependencies: + clickable-path: 0.1.1 + errx: 0.2.2 + fnv1a-64: 0.1.2 + optionalDependencies: + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + + nanoid@3.3.20: {} + + nanotar@0.3.0: {} + + nitropack@2.13.4(rolldown@1.2.14)(srvx@1.0.6)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)): + dependencies: + '@cloudflare/kv-asset-handler': 0.4.2 + '@rollup/plugin-alias': 6.0.0(rollup@4.64.9) + '@rollup/plugin-commonjs': 29.0.3(rollup@4.64.9) + '@rollup/plugin-inject': 5.0.5(rollup@4.64.9) + '@rollup/plugin-json': 6.1.0(rollup@4.64.9) + '@rollup/plugin-node-resolve': 16.0.3(rollup@4.64.9) + '@rollup/plugin-replace': 6.0.3(rollup@4.64.9) + '@rollup/plugin-terser': 1.0.0(rollup@4.64.9) + '@vercel/nft': 1.11.0(rollup@4.64.9) + archiver: 7.0.1 + c12: 3.3.4(magicast@0.5.5) + chokidar: 5.0.0 + citty: 0.2.2 + compatx: 0.2.0 + confbox: 0.2.4 + consola: 3.4.2 + cookie-es: 2.0.1 + croner: 10.0.1 + crossws: 0.3.5 + db0: 0.3.4 + defu: 6.1.7 + destr: 2.0.5 + dot-prop: 10.2.0 + esbuild: 0.28.2 + escape-string-regexp: 5.0.0 + etag: 1.8.1 + exsolve: 1.1.3 + globby: 16.2.4 + gzip-size: 7.0.0 + h3: 1.15.11 + hookable: 5.5.3 + httpxy: 0.5.5 + ioredis: 5.11.1 + jiti: 2.7.0 + klona: 2.0.6 + knitwork: 1.3.0 + listhen: 1.10.1(srvx@1.0.6) + magic-string: 0.30.21 + magicast: 0.5.5 + mime: 4.1.1 + mlly: 1.8.2 + node-fetch-native: 1.6.7 + node-mock-http: 1.0.5 + ofetch: 1.5.1 + ohash: 2.0.12 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.3 + pretty-bytes: 7.2.0 + radix3: 1.1.2 + rollup: 4.64.9 + rollup-plugin-visualizer: 7.1.1(rolldown@1.2.14)(rollup@4.64.9) + scule: 1.3.0 + semver: 7.8.5 + serve-placeholder: 2.0.2 + serve-static: 2.2.1 + source-map: 0.7.6 + std-env: 4.3.0 + ufo: 1.6.4 + ultrahtml: 1.7.0 + uncrypto: 0.1.3 + unctx: 2.5.0 + unenv: 2.0.0-rc.24 + unimport: 6.5.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + unplugin-utils: 0.3.2 + unstorage: 1.17.6(db0@0.3.4)(ioredis@5.11.1) + untyped: 2.0.0 + unwasm: 0.5.3 + youch: 4.1.1 + youch-core: 0.3.3 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@farmfe/core' + - '@libsql/client' + - '@netlify/blobs' + - '@parcel/watcher' + - '@planetscale/database' + - '@rsbuild/core' + - '@rspack/core' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bare-abort-controller + - bare-buffer + - better-sqlite3 + - bun-types-no-globals + - drizzle-orm + - encoding + - idb-keyval + - mysql2 + - oxc-parser + - react-native-b4a + - rolldown + - sqlite3 + - srvx + - supports-color + - unloader + - uploadthing + - vite + - webpack + + node-fetch-native@1.6.7: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-forge@1.4.0: {} + + node-gyp-build@4.8.4: {} + + node-mock-http@1.0.5: {} + + node-releases@2.0.59: {} + + nopt@8.1.0: + dependencies: + abbrev: 3.0.1 + + normalize-path@3.0.0: {} + + nostics@1.3.0: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.154.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.9))(rollup@4.64.9)(srvx@1.0.6)(terser@5.51.2)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)): + dependencies: + '@dxup/nuxt': 0.5.10(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(jiti@2.7.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + '@jridgewell/trace-mapping': 0.3.31 + '@nuxt/cli': 4.0.1(@nuxt/schema@4.6.2)(jiti@2.7.0)(rolldown@1.2.14)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + '@nuxt/devalue': 2.0.2 + '@nuxt/devtools': 3.4.3(@nuxt/schema@4.6.2)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44) + '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))) + '@nuxt/nitro-server': 4.6.2(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@nuxt/schema@4.6.2)(@oxc-project/types@0.154.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(jiti@2.7.0)(lightningcss@1.33.0)(magic-string@1.4.3)(mlly@1.8.2)(nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.154.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.9))(rollup@4.64.9)(srvx@1.0.6)(terser@5.51.2)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(rolldown@1.2.14)(rollup@4.64.9)(srvx@1.0.6)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + '@nuxt/schema': 4.6.2 + '@nuxt/telemetry': 2.9.1(@nuxt/kit@4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))) + '@nuxt/vite-builder': 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.154.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.9))(rollup@4.64.9)(srvx@1.0.6)(terser@5.51.2)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.9))(terser@5.51.2)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(vue@3.5.44) + '@nuxt/vite-server': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.154.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.9))(rollup@4.64.9)(srvx@1.0.6)(terser@5.51.2)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)))(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + '@unhead/vue': 3.4.3(@oxc-project/types@0.154.0)(esbuild@0.28.2)(lightningcss@1.33.0)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44) + '@vue/shared': 3.5.44 + chokidar: 5.0.0 + clickable-path: 0.1.1 + compatx: 0.2.0 + consola: 3.4.2 + cookie-es: 3.1.1 + defu: 6.1.7 + destr: 2.0.5 + devalue: 6.0.3 + errx: 0.2.2 + escape-string-regexp: 5.0.0 + exsolve: 1.1.3 + fetchdts: 0.2.0 + fnv1a-64: 0.1.2 + hookable: 6.1.2 + ignore: 7.0.12 + impound: 1.2.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + iron-webcrypto: 2.0.0 + jiti: 2.7.0 + klona: 2.0.6 + knitwork: 1.3.0 + magic-string: 1.4.3 + my-bad: 0.2.11(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + nanotar: 0.3.0 + nostics: 1.3.0 + object-identity: 0.2.3 + ofetch: 1.5.1 + ohash: 2.0.12 + on-change: 6.0.2 + oxc-walker: 1.1.1(@oxc-project/types@0.154.0)(rolldown@1.2.14) + pathe: 2.0.3 + perfect-debounce: 2.1.0 + picomatch: 4.0.7 + pkg-types: 2.3.3 + rolldown: 1.2.14 + rolldown-string: 0.3.1(rolldown@1.2.14) + rou3: 0.9.2 + scule: 1.3.0 + std-env: 4.3.0 + tinyglobby: 0.2.17 + ufo: 1.6.4 + ultrahtml: 1.7.0 + uncrypto: 0.1.3 + unctx: 3.0.1(magic-string@1.4.3)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))) + undici: 8.12.2 + unhead: 3.4.3(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + unimport: 7.0.2(acorn@8.19.0)(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + unplugin: 3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + unrouting: 0.2.3 + untyped: 2.0.0 + verkit: 0.5.0 + vue: 3.5.44 + vue-bundle-renderer: 2.4.0 + vue-component-type-helpers: 3.3.13 + vue-router: 5.4.0(@vue/compiler-sfc@3.5.44)(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@babel/plugin-proposal-decorators' + - '@babel/plugin-syntax-jsx' + - '@babel/plugin-syntax-typescript' + - '@biomejs/biome' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@farmfe/core' + - '@libsql/client' + - '@netlify/blobs' + - '@nuxt/docs' + - '@oxc-project/types' + - '@pinia/colada' + - '@planetscale/database' + - '@rollup/plugin-babel' + - '@rsbuild/core' + - '@rspack/core' + - '@unhead/cli' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - '@vitejs/devtools' + - '@vitejs/devtools-kit' + - '@vue/compiler-sfc' + - acorn + - aws4fetch + - bare-abort-controller + - bare-buffer + - better-sqlite3 + - bufferutil + - bun-types-no-globals + - confbox + - db0 + - dotenv + - drizzle-orm + - encoding + - esbuild + - eslint + - giget + - idb-keyval + - ioredis + - less + - lightningcss + - meow + - mlly + - mysql2 + - optionator + - oxc-parser + - oxlint + - pinia + - react-native-b4a + - rollup + - rollup-plugin-visualizer + - sass + - sass-embedded + - serve + - sqlite3 + - srvx + - stylelint + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - unloader + - uploadthing + - utf-8-validate + - vite + - vue-tsc + - webpack + - xml2js + - yaml + + object-identity@0.2.3: {} + + obug@2.2.1: {} + + obug@3.0.0: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.4 + + ohash@2.0.12: {} + + on-change@6.0.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + open@11.0.4: + dependencies: + default-browser: 5.5.1 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.2.1 + wsl-utils: 1.0.1 + + oxc-walker@1.1.1(@oxc-project/types@0.154.0)(rolldown@1.2.14): + optionalDependencies: + '@oxc-project/types': 0.154.0 + rolldown: 1.2.14 + + package-json-from-dist@1.0.1: {} + + package-manager-detector@1.9.0: {} + + parseurl@1.3.3: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.3 + minipass: 7.1.3 + + pathe@2.0.3: {} + + perfect-debounce@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + pkg-types@2.3.3: + dependencies: + confbox: 0.3.1 + exsolve: 1.1.3 + pathe: 2.0.3 + + postcss-calc@11.2.3(postcss@8.5.30): + dependencies: + '@csstools/css-tokenizer': 4.0.2 + postcss: 8.5.30 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.30: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + powershell-utils@0.2.1: {} + + pretty-bytes@7.2.0: {} + + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + quansync@0.2.11: {} + + queue-microtask@1.2.3: {} + + radix3@1.1.2: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + range-parser@1.3.0: {} + + rc9@3.1.0: + dependencies: + defu: 6.1.7 + destr: 2.0.5 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-stream@4.8.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.9 + + readdirp@5.1.1: {} + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + resolve-from@5.0.0: {} + + resolve@1.22.13: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.17.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + retry@0.12.0: {} + + reusify@1.1.0: {} + + rolldown-string@0.3.1(rolldown@1.2.14): + dependencies: + magic-string: 1.4.3 + optionalDependencies: + rolldown: 1.2.14 + + rolldown@1.2.14: + dependencies: + '@oxc-project/types': 0.154.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.14 + '@rolldown/binding-android-arm64': 1.2.14 + '@rolldown/binding-darwin-arm64': 1.2.14 + '@rolldown/binding-darwin-x64': 1.2.14 + '@rolldown/binding-freebsd-x64': 1.2.14 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.14 + '@rolldown/binding-linux-arm64-gnu': 1.2.14 + '@rolldown/binding-linux-arm64-musl': 1.2.14 + '@rolldown/binding-linux-ppc64-gnu': 1.2.14 + '@rolldown/binding-linux-s390x-gnu': 1.2.14 + '@rolldown/binding-linux-x64-gnu': 1.2.14 + '@rolldown/binding-linux-x64-musl': 1.2.14 + '@rolldown/binding-openharmony-arm64': 1.2.14 + '@rolldown/binding-win32-arm64-msvc': 1.2.14 + '@rolldown/binding-win32-x64-msvc': 1.2.14 + + rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.9): + dependencies: + open: 11.0.4 + picomatch: 4.0.7 + source-map: 0.8.0 + yargs: 18.2.0 + optionalDependencies: + rolldown: 1.2.14 + rollup: 4.64.9 + + rollup@4.64.9: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.64.9 + '@rollup/rollup-android-arm64': 4.64.9 + '@rollup/rollup-darwin-arm64': 4.64.9 + '@rollup/rollup-darwin-x64': 4.64.9 + '@rollup/rollup-freebsd-arm64': 4.64.9 + '@rollup/rollup-freebsd-x64': 4.64.9 + '@rollup/rollup-linux-arm-gnueabihf': 4.64.9 + '@rollup/rollup-linux-arm-musleabihf': 4.64.9 + '@rollup/rollup-linux-arm64-gnu': 4.64.9 + '@rollup/rollup-linux-arm64-musl': 4.64.9 + '@rollup/rollup-linux-loong64-gnu': 4.64.9 + '@rollup/rollup-linux-loong64-musl': 4.64.9 + '@rollup/rollup-linux-ppc64-gnu': 4.64.9 + '@rollup/rollup-linux-ppc64-musl': 4.64.9 + '@rollup/rollup-linux-riscv64-gnu': 4.64.9 + '@rollup/rollup-linux-riscv64-musl': 4.64.9 + '@rollup/rollup-linux-s390x-gnu': 4.64.9 + '@rollup/rollup-linux-x64-gnu': 4.64.9 + '@rollup/rollup-linux-x64-musl': 4.64.9 + '@rollup/rollup-openbsd-x64': 4.64.9 + '@rollup/rollup-openharmony-arm64': 4.64.9 + '@rollup/rollup-win32-arm64-msvc': 4.64.9 + '@rollup/rollup-win32-ia32-msvc': 4.64.9 + '@rollup/rollup-win32-x64-gnu': 4.64.9 + '@rollup/rollup-win32-x64-msvc': 4.64.9 + fsevents: 2.3.3 + + rou3@0.9.2: {} + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + scule@1.3.0: {} + + secure-random-bytes@5.0.1: + dependencies: + secure-random-octet: 4.0.1 + + secure-random-octet@4.0.1: + dependencies: + get-random-values: 3.0.0 + + semver@6.3.1: {} + + semver@7.8.5: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serialize-javascript@7.1.2: {} + + seroval@1.6.11: {} + + serve-placeholder@2.0.2: + dependencies: + defu: 6.1.7 + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.12.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + sisteransi@1.0.5: {} + + slash@5.1.0: {} + + smob@1.6.2: {} + + source-map-js@1.2.2: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.6: {} + + source-map@0.8.0: {} + + srvx@1.0.5: {} + + srvx@1.0.6: {} + + standard-as-callback@2.1.0: {} + + statuses@2.0.2: {} + + std-env@4.3.0: {} + + stream-browserify@3.0.0: + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + + streamx@2.28.1: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.7.0 + strip-ansi: 7.2.0 + + string-width@8.3.0: + dependencies: + get-east-asian-width: 1.7.0 + strip-ansi: 7.2.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.4.0 + + strip-final-newline@3.0.0: {} + + strip-literal@4.0.0: + dependencies: + js-tokens: 10.0.0 + + structured-clone-es@2.0.1: {} + + supports-color@10.2.2: {} + + supports-preserve-symlinks-flag@1.0.0: {} + + tagged-tag@1.0.0: {} + + tar-stream@3.2.2: + dependencies: + b4a: 1.9.0 + bare-fs: 4.8.4 + fast-fifo: 1.3.2 + streamx: 2.28.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar@7.5.22: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + + teex@1.0.1: + dependencies: + streamx: 2.28.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + terser@5.51.2: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.19.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + text-decoder@1.2.7: + dependencies: + b4a: 1.9.0 + transitivePeerDependencies: + - react-native-b4a + + tiny-invariant@1.3.3: {} + + tinyclip@0.1.15: {} + + tinyclip@1.0.3: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + totalist@3.0.1: {} + + tr46@0.0.3: {} + + tslib@2.8.1: {} + + type-fest@5.10.0: + dependencies: + tagged-tag: 1.0.0 + + ufo@1.6.4: {} + + uint8array-extras@1.6.0: {} + + ultrahtml@1.7.0: {} + + uncrypto@0.1.3: {} + + unctx@2.5.0: + dependencies: + acorn: 8.19.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + unplugin: 2.3.11 + + unctx@3.0.1(magic-string@1.4.3)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))): + optionalDependencies: + magic-string: 1.4.3 + rolldown: 1.2.14 + unplugin: 3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + + undici@8.12.2: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + unhead@3.4.3(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)): + dependencies: + hookable: 6.1.2 + unplugin: 3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + optionalDependencies: + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + transitivePeerDependencies: + - '@farmfe/core' + - '@rsbuild/core' + - '@rspack/core' + - bun-types-no-globals + - esbuild + - rolldown + - rollup + - unloader + - webpack + + unicorn-magic@0.3.0: {} + + unicorn-magic@0.4.1: {} + + unimport@6.5.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)): + dependencies: + acorn: 8.19.0 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + local-pkg: 1.2.1 + magic-string: 1.4.3 + mlly: 1.8.2 + pathe: 2.0.3 + picomatch: 4.0.7 + pkg-types: 2.3.3 + scule: 1.3.0 + strip-literal: 4.0.0 + tinyglobby: 0.2.17 + unplugin: 3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + unplugin-utils: 0.3.2 + optionalDependencies: + rolldown: 1.2.14 + transitivePeerDependencies: + - '@farmfe/core' + - '@rsbuild/core' + - '@rspack/core' + - bun-types-no-globals + - esbuild + - rollup + - unloader + - vite + - webpack + + unimport@7.0.2(acorn@8.19.0)(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)): + dependencies: + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + local-pkg: 1.2.1 + magic-string: 1.4.3 + mlly: 1.8.2 + pathe: 2.0.3 + picomatch: 4.0.7 + pkg-types: 2.3.3 + scule: 1.3.0 + strip-literal: 4.0.0 + tinyglobby: 0.2.17 + unplugin: 3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + unplugin-utils: 0.3.2 + optionalDependencies: + acorn: 8.19.0 + rolldown: 1.2.14 + transitivePeerDependencies: + - '@farmfe/core' + - '@rsbuild/core' + - '@rspack/core' + - bun-types-no-globals + - esbuild + - rollup + - unloader + - vite + - webpack + + unplugin-utils@0.3.2: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.7 + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.19.0 + picomatch: 4.0.7 + webpack-virtual-modules: 0.6.2 + + unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)): + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.7 + webpack-virtual-modules: 0.6.2 + optionalDependencies: + esbuild: 0.28.2 + rolldown: 1.2.14 + rollup: 4.64.9 + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + + unrouting@0.2.3: + dependencies: + escape-string-regexp: 5.0.0 + ufo: 1.6.4 + + unstorage@1.17.6(db0@0.3.4)(ioredis@5.11.1): + dependencies: + anymatch: 3.1.3 + chokidar: 5.0.0 + destr: 2.0.5 + h3: 1.15.11 + lru-cache: 11.5.3 + node-fetch-native: 1.6.7 + ofetch: 1.5.1 + ufo: 1.6.4 + optionalDependencies: + db0: 0.3.4 + ioredis: 5.11.1 + + untun@0.2.2: {} + + untyped@2.0.0: + dependencies: + citty: 0.1.6 + defu: 6.1.7 + jiti: 2.7.0 + knitwork: 1.3.0 + scule: 1.3.0 + + unwasm@0.5.3: + dependencies: + exsolve: 1.1.3 + knitwork: 1.3.0 + magic-string: 0.30.21 + mlly: 1.8.2 + pathe: 2.0.3 + pkg-types: 2.3.3 + + update-browserslist-db@1.3.4(browserslist@4.29.3): + dependencies: + browserslist: 4.29.3 + escalade: 3.2.0 + picocolors: 1.1.1 + + uqr@0.1.3: {} + + util-deprecate@1.0.2: {} + + uuid@11.1.1: {} + + verkit@0.5.0: {} + + vite-dev-rpc@2.0.0(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)): + dependencies: + birpc: 4.2.0 + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + vite-hot-client: 2.2.0(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + + vite-hot-client@2.2.0(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)): + dependencies: + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + + vite-node@6.0.0(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2): + dependencies: + cac: 7.1.0 + es-module-lexer: 2.3.2 + obug: 2.2.1 + pathe: 2.0.3 + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + transitivePeerDependencies: + - '@types/node' + - '@vitejs/devtools' + - esbuild + - jiti + - less + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + vite-plugin-checker@0.14.5(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)): + dependencies: + '@babel/code-frame': 7.29.7 + chokidar: 5.0.0 + npm-run-path: 6.0.0 + picocolors: 1.1.1 + picomatch: 4.0.7 + proper-lockfile: 4.1.2 + tiny-invariant: 1.3.3 + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + + vite-plugin-inspect@11.4.1(@nuxt/kit@4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))))(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)): + dependencies: + ansis: 4.4.0 + error-stack-parser-es: 1.0.5 + obug: 2.2.1 + ohash: 2.0.12 + open: 11.0.4 + perfect-debounce: 2.1.0 + sirv: 3.0.2 + unplugin-utils: 0.3.2 + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + vite-dev-rpc: 2.0.0(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + optionalDependencies: + '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))) + + vite-plugin-vue-tracer@1.6.0(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44): + dependencies: + estree-walker: 3.0.3 + exsolve: 1.1.3 + magic-string: 1.4.3 + pathe: 2.0.3 + source-map-js: 1.2.2 + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + vue: 3.5.44 + + vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.30 + rolldown: 1.2.14 + tinyglobby: 0.2.17 + optionalDependencies: + esbuild: 0.28.2 + fsevents: 2.3.3 + jiti: 2.7.0 + terser: 5.51.2 + + vue-bundle-renderer@2.4.0: + dependencies: + ufo: 1.6.4 + + vue-component-type-helpers@3.3.13: {} + + vue-devtools-stub@0.1.0: {} + + vue-router@5.4.0(@vue/compiler-sfc@3.5.44)(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2))(vue@3.5.44): + dependencies: + '@vue-macros/common': 3.1.4(vue@3.5.44) + '@vue/devtools-api': 8.2.1 + ast-walker-scope: 0.10.0 + chokidar: 5.0.0 + confbox: 0.3.1 + local-pkg: 1.2.1 + magic-string: 1.4.3 + mlly: 1.8.2 + muggle-string: 0.4.1 + nostics: 1.3.0 + pathe: 2.0.3 + picomatch: 4.0.7 + scule: 1.3.0 + tinyglobby: 0.2.17 + unplugin: 3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.9)(vite@8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)) + unplugin-utils: 0.3.2 + vue: 3.5.44 + optionalDependencies: + '@vue/compiler-sfc': 3.5.44 + vite: 8.3.4(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2) + transitivePeerDependencies: + - '@farmfe/core' + - '@rsbuild/core' + - '@rspack/core' + - bun-types-no-globals + - esbuild + - rolldown + - rollup + - unloader + - webpack + + vue@3.5.44: + dependencies: + '@vue/compiler-dom': 3.5.44 + '@vue/compiler-sfc': 3.5.44 + '@vue/runtime-dom': 3.5.44 + '@vue/server-renderer': 3.5.44 + '@vue/shared': 3.5.44 + + webidl-conversions@3.0.1: {} + + webpack-virtual-modules@0.6.2: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@6.0.1: + dependencies: + isexe: 4.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + ws@8.22.0: {} + + wsl-utils@1.0.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.2.1 + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@5.0.0: {} + + yargs-parser@22.0.0: {} + + yargs@18.2.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 8.3.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.1: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.7.0 + '@speed-highlight/core': 1.2.24 + cookie-es: 3.1.1 + youch-core: 0.3.3 + + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.8.0 diff --git a/quickstarts/nuxt/pnpm-workspace.yaml b/quickstarts/nuxt/pnpm-workspace.yaml new file mode 100644 index 0000000..69524b2 --- /dev/null +++ b/quickstarts/nuxt/pnpm-workspace.yaml @@ -0,0 +1,18 @@ +allowBuilds: + core-js: false + esbuild: true + +auditConfig: + ignoreGhsas: + # JUSTIFICATION: GHSA-86w9-cpqp-85rv β€” node-forge accepts extra nested DigestAlgorithm elements + # during RSA PKCS#1 v1.5 signature verification. No patched release exists (1.4.0 is the latest + # and is affected). Transitive via nuxt's dev tooling (`@nuxt/cli` / `nitropack` > `listhen` > + # `node-forge`), which only uses it to generate RSA keys and self-signed certificates for the + # local HTTPS dev server; it never verifies signatures. Revisit once node-forge publishes a fix. + - GHSA-86w9-cpqp-85rv + # JUSTIFICATION: GHSA-vfj7-8cjw-p6xm β€” braces stack-exhaustion DoS through deeply nested brace + # patterns. No patched release exists (3.0.3 is the latest and is affected). Transitive via + # nuxt's build tooling (`@nuxt/nitro-server` > `nitropack` > `globby` > `fast-glob` > + # `micromatch` > `braces`), which only expands glob patterns taken from the project's own config + # and filesystem at build time. Revisit once braces publishes a fix. + - GHSA-vfj7-8cjw-p6xm diff --git a/quickstarts/nuxt/scripts/prepare-dev.cjs b/quickstarts/nuxt/scripts/prepare-dev.cjs new file mode 100644 index 0000000..83648f6 --- /dev/null +++ b/quickstarts/nuxt/scripts/prepare-dev.cjs @@ -0,0 +1,73 @@ +// 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 PREFIX = 'NUXT_PUBLIC_THUNDERID_'; +const NATIVE_FLOW_VARS = [`${PREFIX}APPLICATION_ID`, `${PREFIX}SIGN_IN_URL`, `${PREFIX}SIGN_UP_URL`]; +const REDIRECT_FLOW_VARS = [`${PREFIX}CLIENT_ID`, 'THUNDERID_CLIENT_SECRET']; +const REDIRECT_FLOW_PLACEHOLDERS = { + [`${PREFIX}CLIENT_ID`]: 'your-client-id-here', + THUNDERID_CLIENT_SECRET: 'your-client-secret-here', +}; + +const flowArg = process.argv.find((arg) => arg.startsWith('--flow=')); +const flowExplicitlyRequested = Boolean(flowArg); +const flow = flowArg ? flowArg.slice('--flow='.length) : 'native'; + +if (flowExplicitlyRequested && flow !== 'native' && flow !== 'redirect') { + console.error(`Unknown --flow value "${flow}". Expected "native" or "redirect".`); + process.exit(1); +} + +/** + * Toggles the leading `# ` on env var lines to match the selected flow, appending + * any vars for the selected flow that aren't present in the source file yet (the + * redirect-flow vars aren't checked into `.env.example`, so switching to `redirect` + * from a fresh copy needs to add them rather than just uncomment them). + */ +function applyFlow(envContent, selectedFlow) { + const varsToEnable = selectedFlow === 'redirect' ? REDIRECT_FLOW_VARS : NATIVE_FLOW_VARS; + const varsToDisable = selectedFlow === 'redirect' ? NATIVE_FLOW_VARS : REDIRECT_FLOW_VARS; + const found = new Set(); + + const lines = envContent.split('\n').map((line) => { + const enable = varsToEnable.find((key) => line.replace(/^#\s*/, '').startsWith(`${key}=`)); + if (enable) { + found.add(enable); + return line.replace(/^#\s*/, ''); + } + + const disable = varsToDisable.find((key) => line.startsWith(`${key}=`)); + if (disable) return `# ${line}`; + + return line; + }); + + const missing = varsToEnable.filter((key) => !found.has(key)); + if (missing.length > 0) { + lines.push('', `# ── ${selectedFlow} flow ─────────────────────────────────────────────────`); + for (const key of missing) lines.push(`${key}=${REDIRECT_FLOW_PLACEHOLDERS[key] ?? ''}`); + } + + return lines.join('\n'); +} + +const envExample = path.join(root, '.env.example'); +const envTarget = path.join(root, '.env'); + +// Only explicit `--flow=` invocations are allowed to (re)write an existing +// .env β€” a plain `npm run prepare-dev` (e.g. from .stackblitzrc's +// startCommand) stays a safe, idempotent copy-if-missing. +if (fs.existsSync(envExample) && (flowExplicitlyRequested || !fs.existsSync(envTarget))) { + const envSource = fs.existsSync(envTarget) ? envTarget : envExample; + const envContent = applyFlow(fs.readFileSync(envSource, 'utf8'), flow) + // 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/quickstarts/react/.env.example b/quickstarts/react/.env.example new file mode 100644 index 0000000..e38f402 --- /dev/null +++ b/quickstarts/react/.env.example @@ -0,0 +1,5 @@ +# Base URL of your ThunderID deployment. ex: https://localhost:8090 +VITE_THUNDERID_BASE_URL=https://localhost:8090 + +# OAuth2 Client ID for this app. ThunderID Console -> your application -> Overview. +VITE_THUNDERID_CLIENT_ID=your-client-id-here diff --git a/quickstarts/react/.gitignore b/quickstarts/react/.gitignore new file mode 100644 index 0000000..9c97bbd --- /dev/null +++ b/quickstarts/react/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +.env diff --git a/quickstarts/react/.stackblitzrc b/quickstarts/react/.stackblitzrc new file mode 100644 index 0000000..5f0a200 --- /dev/null +++ b/quickstarts/react/.stackblitzrc @@ -0,0 +1,4 @@ +{ + "installDependencies": false, + "startCommand": "npm run prepare-dev && npm install && npm run dev" +} diff --git a/quickstarts/react/README.md b/quickstarts/react/README.md new file mode 100644 index 0000000..2a822bb --- /dev/null +++ b/quickstarts/react/README.md @@ -0,0 +1,36 @@ +# ThunderID React Quickstart + +Open in StackBlitz + +A minimal React + Vite application demonstrating ThunderID authentication with OAuth 2.0, PKCE, and JWT support using the `@thunderid/react` SDK. + +## Prerequisites + +- Node.js 18+ +- pnpm +- A ThunderID application + +## Setup + +1. Copy the environment template: + ```sh + cp .env.example .env + ``` + +2. Fill in your ThunderID credentials in `.env`: + ``` + VITE_THUNDERID_CLIENT_ID= + VITE_THUNDERID_BASE_URL=https://your-thunderid-instance + ``` + +3. Install dependencies and start the dev server: + ```sh + pnpm install + pnpm dev + ``` + +The app will be available at `http://localhost:5173`. + +## Docs + +Full SDK reference: [thunderid.dev/docs](https://thunderid.dev/docs) diff --git a/quickstarts/react/index.html b/quickstarts/react/index.html new file mode 100644 index 0000000..562e0aa --- /dev/null +++ b/quickstarts/react/index.html @@ -0,0 +1,13 @@ + + + + + + + ThunderID React Quickstart + + +
    + + + diff --git a/quickstarts/react/package.json b/quickstarts/react/package.json new file mode 100644 index 0000000..f8e227e --- /dev/null +++ b/quickstarts/react/package.json @@ -0,0 +1,23 @@ +{ + "name": "@thunderid/react-quickstart", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "prepare-dev": "node scripts/prepare-dev.cjs", + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@thunderid/react": "1.4.0", + "@thunderid/react-router": "1.3.0", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "react-router": "^7.18.2" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.5.2", + "vite": "^6.3.5" + } +} diff --git a/quickstarts/react/pnpm-lock.yaml b/quickstarts/react/pnpm-lock.yaml new file mode 100644 index 0000000..b19b918 --- /dev/null +++ b/quickstarts/react/pnpm-lock.yaml @@ -0,0 +1,1647 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@thunderid/react': + specifier: 1.4.0 + version: 1.4.0(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@thunderid/react-router': + specifier: 1.3.0 + version: 1.3.0(@thunderid/browser@1.3.0)(@thunderid/react@1.4.0(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(react-router@7.18.4(react-dom@19.3.1(react@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) + react-router: + specifier: ^7.18.2 + version: 7.18.4(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + devDependencies: + '@vitejs/plugin-react': + specifier: ^4.5.2 + version: 4.7.0(vite@6.4.4) + vite: + specifier: ^6.3.5 + version: 6.4.4 + +packages: + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.10': + resolution: {integrity: sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.10': + resolution: {integrity: sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.10': + resolution: {integrity: sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/css@11.13.5': + resolution: {integrity: sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/react-dom@2.1.9': + resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.27.12': + resolution: {integrity: sha512-kKlWNrpIQxF1B/a2MZvE0/uyKby4960yjO91W7nVyNKmmfNi62xU9HCjL1M1eWzx/LFj/VPSwJVbwQk9Pq/68A==} + peerDependencies: + react: '>=17.0.0' + react-dom: '>=17.0.0' + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.64.9': + resolution: {integrity: sha512-qJmhckvOleXp8R+BaXiVW/S34/3M7pvN17/QcG90A74BDaSvKv8ffY+GYG+a/xU0vEoITzdMSiM7o9HZSQWbfA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.64.9': + resolution: {integrity: sha512-dqQgds39BNWGB2ughJ6n2LTQFXk8RAOwnXj6m387OHpygfD93QRS1ODIfpiWyAa9gsFB9l34pSqeLmA0Xk/VBw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.64.9': + resolution: {integrity: sha512-scrMx4SGikOSbWknLmPD0v6OeJ1Wih3kyRLdNZnlVbBeKVWp1nRTVcpM0IxDJdc3u8UjesPHTmnX649u8cQpBw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.64.9': + resolution: {integrity: sha512-fkwi2OQR+QWoViRFt08Bsca6G0e9pk/4z+JgmqHq6jSRTT0LtI27KRVK8bu++6L/CBtQ3OZePbqC3bcrwGzTqQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.64.9': + resolution: {integrity: sha512-8Apr0Khn0r2MUsDtcOMbp85fDdXsX/E+iLhO17WUjG24m3O8ChArqI0isI7CAdj9Ouq177PHtVtEUpMNE1C4Gw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.64.9': + resolution: {integrity: sha512-H+upy4DhBcBZlUsCAMyr8wR0Dg9kj8sGloVZdZ7va2heJM/5jnROxI47dtXseJjlD/VoQe6MrXhY3b5ikOq/Vw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.64.9': + resolution: {integrity: sha512-b50NS9TbAEK/b16vBoKrkeNwy+HvGBXmtBbzX4wcodWOh9dQZvRTN9rUyElIXa36QnwXhU9PAVVkZpqRzSi2Tg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.64.9': + resolution: {integrity: sha512-SdqcudB/FzjLESsRMgXhV+FXGtf6usxXwlMoRW7D/XS3f9CZ4T4wycRBu2h9bVl2AOaPpunoA8ftAM/MF4altQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.64.9': + resolution: {integrity: sha512-dg19FFRoovN+KZ+gMHBAq4Do8PgQmBcxpuhux57OuiR0QLavfn8uPBY8CP+UO50bO+xf0tK0GlqI/jld++5E6g==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.64.9': + resolution: {integrity: sha512-+jyv5ofGmpAvtrzLjSr/e4syskeikEp5L82bUzB76auwOgK1VNVwZV/F2g1Pes9bhTznbejOefAqPglHSKYmTA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.64.9': + resolution: {integrity: sha512-MKFXxqmkzCbtNvx/vjiJdss44eRtVabhCtDWcCFD9NHGwP+YkqnM/slnEsv/3fsoq9ukTtudraAjGhptrUPCVg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.64.9': + resolution: {integrity: sha512-lhARcOT6n7Ni9hpQRq8LZjbuoW+G/GXaW4Jfhx2MbmjnCj9XxLMRTceJIMpiMLQVWm+qxe7oq1sYvlIGOAW34Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.64.9': + resolution: {integrity: sha512-oORQzGxZ1oHghJwLdwc8Ni0SY9otILAeXsWdiImTF4F5HkWJUwh5s/269PF5tYILNC1z/3RwFsrgcHPguBcb0g==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.64.9': + resolution: {integrity: sha512-J2JQHQt7oR1uutd2OSwbV2Qpb2LI5gggPolbkUwDe7vn/0dnduZ0w6EIbGnZUAw7uIKaMgfVylcH6V+Aov8kRA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.64.9': + resolution: {integrity: sha512-bWaKLrfzeQ9mOzJJxXbPgw2RH6VAaTJrb/60Tqf7YSMxubG8780Hzm7Us+D6qVW8QRGJYl4VUvVXxnRB0o9zvQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.64.9': + resolution: {integrity: sha512-TFJyaCB9NsvSyvVGQgTSRD9XgxsAq99ivDDkt9KAySlrNW5Obsd1kQpvoCvBCOLq+JlJTjmtroLH0wp8M8mEsA==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.64.9': + resolution: {integrity: sha512-DvmOM5xz/wDnUJFVjqP32ahalxn7BDuvO25bpuFOrb50AIFCCcFPN4lWWUDD1c8Ddv9sMO9SXyqtOet8G8u9cQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.64.9': + resolution: {integrity: sha512-aFzokHK3yE00mzAvKm/PY76OsOOTjvoTGE82Vp89iBDyD2wUBlV4ZsR2tWkRECTEFk6TC4Yc3zaEWjPqq1vstQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.64.9': + resolution: {integrity: sha512-t/yMdeaaQUuNNpmT7taBmH2VvHzgT4YA/IaKVE3qVr4PYQemn/kmJkL4uG4UvTLcWXQd0MSoOa/3URYSWGmUEg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.64.9': + resolution: {integrity: sha512-4dnxKQ8GHARyjqAU87q5UWhywZTpIbR2VWhtACdC37GD1SGszmvb3TaBFVA4n+fnDHSKMut4ZJ63hVFwgnxVtg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.64.9': + resolution: {integrity: sha512-1qdQj6IrNoR0xFvrCQHmhBXe9HMbBGNTFi+3n4jKMt2rTx4k0aeAdraZU4WDOMfL9tAwS8/TRSuquvNuPQ+Gpw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.64.9': + resolution: {integrity: sha512-yc1GSEHIYhKGN5WSaJc+eMy8bZvzcvAmflEgFh8L3xcy8dYxGmfrudyBnBFde9f6JzOa1kFqocF8BHaWybjQoA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.64.9': + resolution: {integrity: sha512-VzHGvosAkLIzrwqvSgJBPDmGFTF/yE8OtMT4H5z8pM1NA0NiPpvTYnUh+9xPd+d5oy/wqF317FmJ1I8Kk8pp0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.64.9': + resolution: {integrity: sha512-FB77txy1PiAcJgG72nOLA6R0S3PRZahs8Spx0z2yseDUkoMmJcrQghiebJ2JHZ74cCOVlX/MYd4dUbJtJJ8fow==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.64.9': + resolution: {integrity: sha512-IoIgQLpK1DbOcQf1vX+i5oe5LXypsCVb1j/mU6BLAbOGmXH4E+j4kMfgZRwPYUohVBurNyVMgIKVM5dR6xA0kA==} + cpu: [x64] + os: [win32] + + '@thunderid/browser@1.3.0': + resolution: {integrity: sha512-O6DHlUyVFfBaNcJDEXACLU4NJMrNjDlqAuzesvJ+8a0Gl5g8WFK53rpx027e/rWICG0vcGe2UVZuhvZq5Z511A==} + + '@thunderid/javascript@1.4.0': + resolution: {integrity: sha512-qHfIFzXOQTN1nhuBT91Tr8bLPvfyEDa7gUQSL7GNcJkcyyKfJ77meIwr91lCme0ySFUnTeKl0Bk+R8iIJiKXBw==} + + '@thunderid/react-router@1.3.0': + resolution: {integrity: sha512-eDrOG7xBdkolS9VV5sbGKrKwplppIyivryOYphLa9gwJ128Rynrg2SKbOj8oHo1CVSvRMihgFoYyQkzbNACtTg==} + peerDependencies: + '@thunderid/browser': '>=0.1.0' + '@thunderid/react': '>=0.1.0' + react: '>=16.8.0' + react-router: '>=6.30.1' + + '@thunderid/react@1.4.0': + resolution: {integrity: sha512-MRSnxgAYCnQKXLaAcWd4oiX7H0RfMVtQr8eeDD8aA+TbJYYt2uSIuSEAgOad7iMbVL+xw174n/S/lw6ekKLBqg==} + peerDependencies: + '@types/react': '>=16.8.0' + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + + baseline-browser-mapping@2.11.29: + resolution: {integrity: sha512-YeesxhUmR4h5y5V8KF8iBCqJb8YFrBJ++XBTeGcWIaQOYXGN19JM0FoiOjAhwuDoln8owMgXcWBmgHtYSkky2w==} + engines: {node: '>=6.0.0'} + hasBin: true + + browserslist@4.29.3: + resolution: {integrity: sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001816: + resolution: {integrity: sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw==} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + core-js@3.42.0: + resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dompurify@3.4.13: + resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==} + + electron-to-chromium@1.5.459: + resolution: {integrity: sha512-4SC9HHSFjO/liAhifpdOL5hG9b4BP7/5G3y3UPZVO8trlAXr1CaUEEJL0gWK3kOcYcZrOA5rkwG45Gra/lTIgg==} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-core-module@2.17.0: + resolution: {integrity: sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==} + engines: {node: '>= 0.4'} + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.59: + resolution: {integrity: sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==} + engines: {node: '>=18'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.30: + resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==} + engines: {node: ^10 || ^12 || >=14} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + react-dom@19.3.1: + resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==} + peerDependencies: + react: ^19.3.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-router@7.18.4: + resolution: {integrity: sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.3.1: + resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==} + engines: {node: '>=0.10.0'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.13: + resolution: {integrity: sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==} + engines: {node: '>= 0.4'} + hasBin: true + + rollup@4.64.9: + resolution: {integrity: sha512-mnPSQINChN7PpT+a5CI0/+87hm/+6zxSKBK1XonrzQ0zFnglaTjhkTP2Br4yNCTcLHUEvrAfZffH57jGf+8xBQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + stream-browserify@3.0.0: + resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tabbable@6.5.0: + resolution: {integrity: sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + update-browserslist-db@1.3.4: + resolution: {integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@6.4.4: + resolution: {integrity: sha512-xGjU/SyjeNyb0E4KxXBvYz/1C+3JXuujvf2hXNOtnlcE+58o5ajd9N2/n2VhkiJvpCP2qQasnUpx5PXNeI+p1Q==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.10 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.29.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.10': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/runtime@7.29.10': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.10': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.29.7 + '@babel/runtime': 7.29.10 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/css@11.13.5': + dependencies: + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + transitivePeerDependencies: + - supports-color + + '@emotion/hash@0.9.2': {} + + '@emotion/memoize@0.9.0': {} + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.2.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/unitless@0.10.0': {} + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/react-dom@2.1.9(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@floating-ui/dom': 1.8.0 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + + '@floating-ui/react@0.27.12(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.9(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@floating-ui/utils': 0.2.12 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + tabbable: 6.5.0 + + '@floating-ui/utils@0.2.12': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.64.9': + optional: true + + '@rollup/rollup-android-arm64@4.64.9': + optional: true + + '@rollup/rollup-darwin-arm64@4.64.9': + optional: true + + '@rollup/rollup-darwin-x64@4.64.9': + optional: true + + '@rollup/rollup-freebsd-arm64@4.64.9': + optional: true + + '@rollup/rollup-freebsd-x64@4.64.9': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.64.9': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.64.9': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.64.9': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.64.9': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.64.9': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.64.9': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-x64-musl@4.64.9': + optional: true + + '@rollup/rollup-openbsd-x64@4.64.9': + optional: true + + '@rollup/rollup-openharmony-arm64@4.64.9': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.64.9': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.64.9': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.64.9': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.64.9': + optional: true + + '@thunderid/browser@1.3.0': + dependencies: + '@thunderid/javascript': 1.4.0 + base64url: 3.0.1 + buffer: 6.0.3 + core-js: 3.42.0 + fast-sha256: 1.3.0 + jose: 6.2.3 + process: 0.11.10 + randombytes: 2.1.0 + stream-browserify: 3.0.0 + tslib: 2.8.1 + + '@thunderid/javascript@1.4.0': + dependencies: + jose: 6.2.3 + tslib: 2.8.1 + + '@thunderid/react-router@1.3.0(@thunderid/browser@1.3.0)(@thunderid/react@1.4.0(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(react-router@7.18.4(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(react@19.3.1)': + dependencies: + '@thunderid/browser': 1.3.0 + '@thunderid/react': 1.4.0(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-router: 7.18.4(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + tslib: 2.8.1 + + '@thunderid/react@1.4.0(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@emotion/css': 11.13.5 + '@floating-ui/react': 0.27.12(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@thunderid/browser': 1.3.0 + '@types/react': 19.3.0 + dompurify: 3.4.13 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/estree@1.0.9': {} + + '@types/parse-json@4.0.2': {} + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + '@types/trusted-types@2.0.7': + optional: true + + '@vitejs/plugin-react@4.7.0(vite@6.4.4)': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.4 + transitivePeerDependencies: + - supports-color + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.29.10 + cosmiconfig: 7.1.0 + resolve: 1.22.13 + + base64-js@1.5.1: {} + + base64url@3.0.1: {} + + baseline-browser-mapping@2.11.29: {} + + browserslist@4.29.3: + dependencies: + baseline-browser-mapping: 2.11.29 + caniuse-lite: 1.0.30001816 + electron-to-chromium: 1.5.459 + node-releases: 2.0.59 + update-browserslist-db: 1.3.4(browserslist@4.29.3) + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001816: {} + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + core-js@3.42.0: {} + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.3 + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + dompurify@3.4.13: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + electron-to-chromium@1.5.459: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-errors@1.3.0: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + fast-sha256@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + find-root@1.1.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + ieee754@1.2.1: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + inherits@2.0.4: {} + + is-arrayish@0.2.1: {} + + is-core-module@2.17.0: + dependencies: + hasown: 2.0.4 + + jose@6.2.3: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + json5@2.2.3: {} + + lines-and-columns@1.2.4: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + ms@2.1.3: {} + + nanoid@3.3.20: {} + + node-releases@2.0.59: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.7 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.30: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + process@0.11.10: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + react-dom@19.3.1(react@19.3.1): + dependencies: + react: 19.3.1 + scheduler: 0.28.0 + + react-refresh@0.17.0: {} + + react-router@7.18.4(react-dom@19.3.1(react@19.3.1))(react@19.3.1): + dependencies: + cookie: 1.1.1 + react: 19.3.1 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.3.1(react@19.3.1) + + react@19.3.1: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + resolve-from@4.0.0: {} + + resolve@1.22.13: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.17.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rollup@4.64.9: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.64.9 + '@rollup/rollup-android-arm64': 4.64.9 + '@rollup/rollup-darwin-arm64': 4.64.9 + '@rollup/rollup-darwin-x64': 4.64.9 + '@rollup/rollup-freebsd-arm64': 4.64.9 + '@rollup/rollup-freebsd-x64': 4.64.9 + '@rollup/rollup-linux-arm-gnueabihf': 4.64.9 + '@rollup/rollup-linux-arm-musleabihf': 4.64.9 + '@rollup/rollup-linux-arm64-gnu': 4.64.9 + '@rollup/rollup-linux-arm64-musl': 4.64.9 + '@rollup/rollup-linux-loong64-gnu': 4.64.9 + '@rollup/rollup-linux-loong64-musl': 4.64.9 + '@rollup/rollup-linux-ppc64-gnu': 4.64.9 + '@rollup/rollup-linux-ppc64-musl': 4.64.9 + '@rollup/rollup-linux-riscv64-gnu': 4.64.9 + '@rollup/rollup-linux-riscv64-musl': 4.64.9 + '@rollup/rollup-linux-s390x-gnu': 4.64.9 + '@rollup/rollup-linux-x64-gnu': 4.64.9 + '@rollup/rollup-linux-x64-musl': 4.64.9 + '@rollup/rollup-openbsd-x64': 4.64.9 + '@rollup/rollup-openharmony-arm64': 4.64.9 + '@rollup/rollup-win32-arm64-msvc': 4.64.9 + '@rollup/rollup-win32-ia32-msvc': 4.64.9 + '@rollup/rollup-win32-x64-gnu': 4.64.9 + '@rollup/rollup-win32-x64-msvc': 4.64.9 + fsevents: 2.3.3 + + safe-buffer@5.2.1: {} + + scheduler@0.28.0: {} + + semver@6.3.1: {} + + set-cookie-parser@2.7.2: {} + + source-map-js@1.2.2: {} + + source-map@0.5.7: {} + + stream-browserify@3.0.0: + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stylis@4.2.0: {} + + supports-preserve-symlinks-flag@1.0.0: {} + + tabbable@6.5.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tslib@2.8.1: {} + + update-browserslist-db@1.3.4(browserslist@4.29.3): + dependencies: + browserslist: 4.29.3 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + vite@6.4.4: + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.30 + rollup: 4.64.9 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + + yallist@3.1.1: {} + + yaml@1.10.3: {} diff --git a/quickstarts/react/pnpm-workspace.yaml b/quickstarts/react/pnpm-workspace.yaml new file mode 100644 index 0000000..151e558 --- /dev/null +++ b/quickstarts/react/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + core-js: false + esbuild: true diff --git a/quickstarts/react/scripts/prepare-dev.cjs b/quickstarts/react/scripts/prepare-dev.cjs new file mode 100644 index 0000000..734e095 --- /dev/null +++ b/quickstarts/react/scripts/prepare-dev.cjs @@ -0,0 +1,19 @@ +// 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'); +if (fs.existsSync(envExample) && !fs.existsSync(envTarget)) { + // 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. + const envContent = fs + .readFileSync(envExample, 'utf8') + .replace(/^([A-Z0-9_]+)=(your-\S*|generate-with-\S*|https:\/\/localhost:8090)$/gm, '$1='); + fs.writeFileSync(envTarget, envContent); +} diff --git a/quickstarts/react/src/App.css b/quickstarts/react/src/App.css new file mode 100644 index 0000000..741c085 --- /dev/null +++ b/quickstarts/react/src/App.css @@ -0,0 +1,1069 @@ +/* ─── Design tokens ─────────────────────────────────────────────── */ +:root { + --color-primary: #3688FF; + --color-navy: #05213F; + --color-bg: #F7F9FC; + --color-card: #ffffff; + --color-border: #DDE3EC; + --color-muted: #5A7085; + --color-text: #05213F; + --nav-bg: rgba(247, 249, 252, 0.92); + --nav-height: 52px; + --radius-card: 12px; + --radius-btn: 8px; + --transition: 0.2s ease; +} + +[data-theme='dark'] { + --color-bg: #080f1c; + --color-card: #0f1a2d; + --color-border: rgba(255, 255, 255, 0.09); + --color-text: #E0EAFF; + --color-muted: rgba(224, 234, 255, 0.48); + --color-navy: #E0EAFF; + --nav-bg: rgba(8, 15, 28, 0.92); +} + +/* ─── App shell ─────────────────────────────────────────────────── */ +.app { + min-height: 100vh; + background: var(--color-bg); + color: var(--color-text); + transition: background var(--transition), color var(--transition); +} + +/* ─── Nav ───────────────────────────────────────────────────────── */ +.nav { + position: fixed; + top: 0; + left: 0; + right: 0; + height: var(--nav-height); + display: flex; + align-items: center; + padding: 0 28px; + background: var(--nav-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid var(--color-border); + z-index: 100; + gap: 12px; +} + +.nav-logo { + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + text-decoration: none; + color: inherit; + flex: 1; +} + +.wordmark { + display: flex; + flex-direction: column; + gap: 0; +} +.wordmark-name { + font-size: 14px; + font-weight: 700; + letter-spacing: -0.03em; + line-height: 1.1; + color: var(--color-text); +} +.wordmark-sub { + font-size: 8.5px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: #3688FF; + line-height: 1; + margin-top: 1.5px; +} + +.nav-actions { + display: flex; + align-items: center; + gap: 8px; +} + +/* ─── Dark-mode toggle ──────────────────────────────────────────── */ +.dark-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: 8px; + border: 1px solid var(--color-border); + background: transparent; + color: var(--color-muted); + cursor: pointer; + font-size: 15px; + transition: border-color var(--transition), color var(--transition), background var(--transition); + flex-shrink: 0; +} + +.dark-toggle:hover { + background: var(--color-border); + color: var(--color-text); +} + +/* ─── Buttons ───────────────────────────────────────────────────── */ +.btn-primary, +.btn-outline, +.btn-ghost { + display: inline-flex; + align-items: center; + justify-content: center; + height: 46px; + padding: 0 24px; + border-radius: var(--radius-btn); + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: opacity var(--transition), background var(--transition), border-color var(--transition); + white-space: nowrap; + text-decoration: none; + font-family: inherit; +} + +.btn-primary { + background: var(--color-primary); + color: #fff; + border: 1.5px solid var(--color-primary); +} + +.btn-primary:hover { + opacity: 0.88; +} + +.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); +} + +.btn-ghost { + background: transparent; + color: var(--color-muted); + border: none; +} +.btn-ghost:hover { + background: rgba(0, 0, 0, 0.04); +} + +/* Nav-sized buttons (slightly shorter) */ +.nav .btn-primary, +.nav .btn-outline, +.nav .btn-ghost { + height: 36px; + padding: 0 16px; + font-size: 14px; +} + +/* ─── Loading ───────────────────────────────────────────────────── */ +.loading-screen { + min-height: calc(100vh - var(--nav-height)); + margin-top: var(--nav-height); + display: flex; + align-items: center; + justify-content: center; + color: var(--color-muted); + font-size: 15px; +} + +/* ─── Hero ──────────────────────────────────────────────────────── */ +.hero { + min-height: calc(100vh - var(--nav-height)); + margin-top: var(--nav-height); + padding: 96px 32px 80px; +} + +.home-shell { + min-height: calc(100vh - var(--nav-height)); + margin-top: var(--nav-height); +} + +.hero-inner { + width: 100%; + max-width: 560px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 0; +} + +.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-muted); + margin-bottom: 28px; +} +.hero-badge-line { + display: block; + height: 1px; + width: 24px; + background: var(--color-border); + flex-shrink: 0; +} + +.hero-title { + font-size: 42px; + font-weight: 700; + letter-spacing: -0.05em; + line-height: 1.1; + color: var(--color-text); + margin: 0 0 18px; +} + +.hero-subtitle { + font-size: 16px; + color: var(--color-muted); + max-width: 420px; + line-height: 1.8; + letter-spacing: -0.01em; + margin: 0 auto 36px; +} + +.hero-ctas { + display: flex; + justify-content: center; + gap: 10px; + margin-bottom: 52px; +} + +.hero-divider { + width: 100%; + border: none; + border-top: 1px solid var(--color-border); + margin: 4px 0; +} + +/* ─── Stats row ─────────────────────────────────────────────────── */ +.hero-stats { + width: 100%; + display: grid; + grid-template-columns: repeat(3, 1fr); +} + +.stat { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 16px 8px; +} + +.stat:not(:last-child) { + border-right: 1px solid var(--color-border); +} + +.stat-value { + font-size: 15px; + font-weight: 700; + color: var(--color-text); +} + +.stat-label { + font-size: 12px; + color: var(--color-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-muted); + line-height: 1.7; + max-width: 420px; + margin: 0 auto; +} + +.config-hint code { + background: rgba(54, 136, 255, 0.08); + 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-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-muted); + line-height: 1.6; + margin-bottom: 14px; +} + +.config-value-row { + display: flex; + align-items: center; + gap: 8px; +} + +.config-value { + flex: 1; + min-width: 0; + 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.08); + border-radius: 4px; + padding: 8px 10px; +} + +.config-value-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--color-muted); + margin-bottom: 4px; +} + +.config-value-group { + display: flex; + flex-direction: column; + gap: 12px; +} + +.config-docs-note { + font-size: 13px; + color: var(--color-muted); + max-width: 420px; + margin: 0 auto; +} + +.config-docs-note a { + color: var(--color-primary); + font-weight: 600; +} + +/* ─── 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-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: var(--font-mono, 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 var(--transition); + 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 var(--transition); + padding-top: 2px; +} + +/* ─── Nav back button ───────────────────────────────────────────── */ +.nav-back-btn { + display: inline-flex; + align-items: center; + gap: 4px; + height: 32px; + padding: 0 10px; + border-radius: 6px; + border: none; + background: transparent; + color: var(--color-muted); + font-size: 13px; + font-family: inherit; + letter-spacing: -0.01em; + cursor: pointer; + transition: background var(--transition), color var(--transition); +} + +.nav-back-btn:hover { + background: var(--color-border); + color: var(--color-text); +} + +/* ─── Account page (Google Account-style left-nav layout) ─────────── */ +.account-page { + display: flex; + align-items: flex-start; + min-height: calc(100vh - var(--nav-height)); + margin-top: var(--nav-height); +} + +.account-sidebar { + width: 280px; + flex-shrink: 0; + padding: 40px 24px; + position: sticky; + top: var(--nav-height); +} + +.account-sidebar-title { + font-size: 22px; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--color-text); + margin: 0 0 24px 16px; +} + +.account-nav { + display: flex; + flex-direction: column; + gap: 2px; +} + +.account-nav-item { + display: flex; + align-items: center; + gap: 14px; + width: 100%; + padding: 11px 16px; + border: none; + border-radius: 20px; + background: transparent; + color: var(--color-muted); + font-size: 14px; + font-weight: 500; + font-family: inherit; + text-align: left; + cursor: pointer; + white-space: nowrap; + transition: background var(--transition), color var(--transition); +} + +.account-nav-item:hover { + background: var(--color-border); + color: var(--color-text); +} + +.account-nav-item--active, +.account-nav-item--active:hover { + background: rgba(54, 136, 255, 0.12); + color: var(--color-primary); +} + +.account-content { + flex: 1; + min-width: 0; + display: flex; + justify-content: center; + padding: 40px 32px 80px; + border-left: 1px solid var(--color-border); +} + +.account-content-inner { + width: 100%; + max-width: 720px; +} + +/* UserProfile's `min-width: 600px` (a floor meant to keep the field grid from getting + cramped in other contexts) fights this page's own responsive layout, refusing to shrink + below 600px even when the viewport is narrower. The Security/Home boxes have no such floor + and already shrink fluidly with their container, so match that here. */ +.account-content .thunderid-user-profile { + min-width: 0; +} + +.account-content-title { + font-size: 26px; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--color-text); + margin: 0 0 6px; +} + +.account-content-subtitle { + font-size: 14px; + color: var(--color-muted); + margin: 0 0 32px; +} + +.account-tiles { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + margin-top: 8px; +} + +.account-tile { + text-align: left; + background: var(--color-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-card); + padding: 22px; + cursor: pointer; + font-family: inherit; + display: flex; + flex-direction: column; + transition: border-color var(--transition); +} + +.account-tile:hover { + border-color: rgba(54, 136, 255, 0.35); +} + +.account-tile-icon { + width: 36px; + height: 36px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 14px; +} + +.account-tile-icon--blue { + background: rgba(54, 136, 255, 0.12); + color: var(--color-primary); +} + +.account-tile-icon--green { + background: rgba(16, 185, 129, 0.12); + color: #10b981; +} + +.account-tile-title { + font-size: 15px; + font-weight: 600; + color: var(--color-text); + margin-bottom: 4px; +} + +.account-tile-desc { + font-size: 12.5px; + color: var(--color-muted); + line-height: 1.5; +} + +.account-security-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.account-security-card { + background: var(--color-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-card); + padding: 20px 22px; +} + +.account-security-card-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.account-security-card-title { + font-size: 14px; + font-weight: 600; + color: var(--color-text); + margin-bottom: 2px; +} + +.account-security-card-desc { + font-size: 12.5px; + color: var(--color-muted); +} + +.account-security-card-cta { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 6px; + padding: 0; + background: none; + border: none; + color: var(--color-primary); + font-size: 13px; + font-weight: 500; + font-family: inherit; + cursor: pointer; + transition: opacity var(--transition); +} + +.account-security-card-cta:hover { + opacity: 0.8; +} + +.account-security-card-chevron { + display: inline-flex; + transition: transform 0.2s; +} + +.account-security-card-chevron--open { + transform: rotate(180deg); +} + +.account-security-card-form { + margin-top: 18px; + padding-top: 18px; + border-top: 1px solid var(--color-border); +} + +@media (max-width: 760px) { + .account-page { + flex-direction: column; + } + .account-sidebar { + width: 100%; + position: static; + padding: 20px 16px 8px; + } + .account-nav { + flex-direction: row; + overflow-x: auto; + } + .account-content { + max-width: 100%; + border-left: none; + border-top: 1px solid var(--color-border); + padding: 24px 20px 60px; + } +} + +/* ─── Token debug page ──────────────────────────────────────────── */ +.token-main { + width: 100%; + max-width: 780px; + margin: 0 auto; + margin-top: var(--nav-height); + padding: 52px 28px 80px; +} + +.token-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 36px; + flex-wrap: wrap; +} + +.token-title { + margin: 0 0 5px; + font-size: 22px; + font-weight: 700; + letter-spacing: -0.04em; + color: var(--color-text); +} + +.token-subtitle { + margin: 0; + font-size: 13px; + color: var(--color-muted); + letter-spacing: -0.01em; +} + +.token-badge { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 6px 12px; + border-radius: 6px; + font-size: 12px; + font-weight: 700; + letter-spacing: -0.01em; + flex-shrink: 0; +} + +.token-badge--expired { + background: rgba(220, 38, 38, 0.08); + border: 1px solid rgba(220, 38, 38, 0.22); + color: #dc2626; +} + +.token-badge--valid { + background: rgba(47, 189, 107, 0.08); + border: 1px solid rgba(47, 189, 107, 0.22); + color: #2fbd6b; +} + +.token-badge-dot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; + flex-shrink: 0; +} + +/* Raw token section */ +.token-raw-section { + margin-bottom: 20px; +} + +.token-raw-label-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.token-section-label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--color-muted); +} + +.token-copy-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 10px; + border-radius: 5px; + background: transparent; + border: 1px solid var(--color-border); + font-size: 11px; + font-weight: 600; + color: var(--color-muted); + cursor: pointer; + font-family: inherit; + letter-spacing: -0.01em; + transition: border-color var(--transition), color var(--transition); +} + +.token-copy-btn:hover { + border-color: #3688FF; + color: #3688FF; +} + +.token-raw { + display: block; + font-size: 11px; + font-family: var(--font-mono, monospace); + word-break: break-all; + line-height: 1.8; + background: #0b1120; + padding: 14px 16px; + border-radius: 8px; + overflow-x: auto; +} + +.token-raw--loading { + color: rgba(255, 255, 255, 0.3); +} + +.token-part--header { color: #ff7b72; } +.token-part--payload { color: #79c0ff; } +.token-part--signature { color: #3fb950; } + +.token-dot { + color: rgba(255, 255, 255, 0.2); +} + +/* Decoded grid */ +.token-decoded-grid { + display: grid; + grid-template-columns: 1fr 1.7fr; + gap: 12px; + margin-bottom: 20px; +} + +.token-code-box { + background: #0b1120; + border-radius: 10px; + overflow: hidden; +} + +.token-code-box-header { + padding: 9px 14px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.09em; + text-transform: uppercase; +} + +.token-code-box-header--header { + color: #ff7b72; + border-bottom: 1px solid rgba(255, 123, 114, 0.15); +} + +.token-code-box-header--payload { + color: #79c0ff; + border-bottom: 1px solid rgba(121, 192, 255, 0.15); +} + +.token-code-pre { + margin: 0; + padding: 14px; + font-size: 11.5px; + line-height: 1.8; + font-family: var(--font-mono, monospace); + color: #c9d1d9; + overflow-x: auto; + max-height: 220px; +} + +/* Meta row */ +.token-meta-row { + display: flex; + gap: 32px; + padding-top: 20px; + border-top: 1px solid var(--color-border); + flex-wrap: wrap; +} + +.token-meta-label { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--color-muted); + margin-bottom: 5px; +} + +.token-meta-value { + font-size: 12px; + font-family: var(--font-mono, monospace); + color: var(--color-text); + letter-spacing: -0.01em; +} diff --git a/quickstarts/react/src/App.jsx b/quickstarts/react/src/App.jsx new file mode 100644 index 0000000..e9b27ab --- /dev/null +++ b/quickstarts/react/src/App.jsx @@ -0,0 +1,22 @@ +import { createBrowserRouter, RouterProvider } from 'react-router' +import { ProtectedRoute } from '@thunderid/react-router' +import Nav from './components/Nav' +import HomePage from './pages/HomePage' +import TokenDebugPage from './pages/TokenDebugPage' +import AccountPage from './pages/AccountPage' +import './App.css' + +const router = createBrowserRouter([ + { + element: