diff --git a/.env b/.env new file mode 100644 index 0000000..ce8cf55 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +# Each demo has separate origin due to relying party restrictions (https://webauthn.wtf/how-it-works/relying-party) +NEXT_PUBLIC_DEFAULT_EXAMPLE_ORIGIN="http://localhost:3000" +NEXT_PUBLIC_UPGRADE_EXAMPLE_ORIGIN="http://localhost:3001" \ No newline at end of file diff --git a/.github/workflows/preview-common.yaml b/.github/workflows/preview-common.yaml new file mode 100644 index 0000000..bbd1766 --- /dev/null +++ b/.github/workflows/preview-common.yaml @@ -0,0 +1,44 @@ +name: Reusable Preview Workflow + +on: + workflow_call: + inputs: + vercel_project_name: + required: true + type: string + vercel_scope: + required: true + type: string + + secrets: + VERCEL_TOKEN: + required: true + SENTRY_AUTH_TOKEN: + required: true + +jobs: + shared-steps: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Enable Corepack + run: corepack enable + + - name: Install Corepack + run: corepack install + + - name: Link Project to Vercel + run: yarn dlx -q vercel link --project=${{ inputs.vercel_project_name }} --scope=${{ inputs.vercel_scope }} --yes --token=${{ secrets.VERCEL_TOKEN }} + + - name: Pull Vercel Environment Information + run: yarn dlx -q vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} + + - name: Build Project Artifacts + run: yarn dlx -q vercel build --token=${{ secrets.VERCEL_TOKEN }} + + - name: Deploy Project Artifacts to Vercel + run: yarn dlx -q vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} diff --git a/.github/workflows/preview.yaml b/.github/workflows/preview.yaml index 9e5d987..6905422 100644 --- a/.github/workflows/preview.yaml +++ b/.github/workflows/preview.yaml @@ -1,37 +1,21 @@ name: Vercel Preview Deployment -env: - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_SCOPE: ${{ secrets.VERCEL_SCOPE }} - VERCEL_PROJECT_NAME: ${{ secrets.VERCEL_PROJECT_NAME }} + on: push: branches: - dev -jobs: - Deploy-Preview: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Enable Corepack - run: corepack enable - - - name: Install Corepack - run: corepack install - - name: Link Project to Vercel - run: yarn dlx -q vercel link --project=$VERCEL_PROJECT_NAME --scope=$VERCEL_SCOPE --yes --token=$VERCEL_TOKEN - - - name: Pull Vercel Environment Information - run: yarn dlx -q vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} - - - name: Build Project Artifacts - run: yarn dlx -q vercel build --token=${{ secrets.VERCEL_TOKEN }} +jobs: + default-example: + uses: ./.github/workflows/preview-common.yaml + secrets: inherit + with: + vercel_project_name: ${{ vars.VERCEL_PROJECT_DEFAULT }} + vercel_scope: ${{ vars.VERCEL_SCOPE }} - - name: Deploy Project Artifacts to Vercel - run: yarn dlx -q vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} + upgrade-example: + uses: ./.github/workflows/preview-common.yaml + secrets: inherit + with: + vercel_project_name: ${{ vars.VERCEL_PROJECT_UPGRADE }} + vercel_scope: ${{ vars.VERCEL_SCOPE }} diff --git a/.github/workflows/production-common.yaml b/.github/workflows/production-common.yaml new file mode 100644 index 0000000..4d255fd --- /dev/null +++ b/.github/workflows/production-common.yaml @@ -0,0 +1,44 @@ +name: Reusable Production Workflow + +on: + workflow_call: + inputs: + vercel_project_name: + required: true + type: string + vercel_scope: + required: true + type: string + + secrets: + VERCEL_TOKEN: + required: true + SENTRY_AUTH_TOKEN: + required: true + +jobs: + shared-steps: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Enable Corepack + run: corepack enable + + - name: Install Corepack + run: corepack install + + - name: Link Project to Vercel + run: yarn dlx -q vercel link --project=${{ inputs.vercel_project_name }} --scope=${{ inputs.vercel_scope }} --yes --token=${{ secrets.VERCEL_TOKEN }} + + - name: Pull Vercel Environment Information + run: yarn dlx -q vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} + + - name: Build Project Artifacts + run: yarn dlx -q vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} + + - name: Deploy Project Artifacts to Vercel + run: yarn dlx -q vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} diff --git a/.github/workflows/production.yaml b/.github/workflows/production.yaml index 15fa5ac..d8d091c 100644 --- a/.github/workflows/production.yaml +++ b/.github/workflows/production.yaml @@ -1,37 +1,20 @@ name: Vercel Production Deployment -env: - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_SCOPE: ${{ secrets.VERCEL_SCOPE }} - VERCEL_PROJECT_NAME: ${{ secrets.VERCEL_PROJECT_NAME }} + on: push: branches: - main -jobs: - Deploy-Preview: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Enable Corepack - run: corepack enable - - - name: Install Corepack - run: corepack install - - name: Link Project to Vercel - run: yarn dlx -q vercel link --project=$VERCEL_PROJECT_NAME --scope=$VERCEL_SCOPE --yes --token=$VERCEL_TOKEN - - - name: Pull Vercel Environment Information - run: yarn dlx -q vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} - - - name: Build Project Artifacts - run: yarn dlx -q vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} +jobs: + default-example: + uses: ./.github/workflows/production-common.yaml + secrets: inherit + with: + vercel_project_name: ${{ vars.VERCEL_PROJECT_DEFAULT }} + vercel_scope: ${{ vars.VERCEL_SCOPE }} - - name: Deploy Project Artifacts to Vercel - run: yarn dlx -q vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} + upgrade-example: + uses: ./.github/workflows/production-common.yaml + with: + vercel_project_name: ${{ secrets.VERCEL_PROJECT_UPGRADE }} + vercel_scope: ${{ vars.VERCEL_SCOPE }} diff --git a/.nvmrc b/.nvmrc index c130222..fdb2eaa 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.18.0 \ No newline at end of file +22.11.0 \ No newline at end of file diff --git a/.yarnrc.yml b/.yarnrc.yml index 1517771..9da7e23 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1,6 +1,6 @@ compressionLevel: 0 -defaultSemverRangePrefix: "" +defaultSemverRangePrefix: '' enableHardenedMode: true @@ -17,3 +17,5 @@ nmSelfReferences: false nodeLinker: node-modules preferInteractive: true + +nmMode: hardlinks-global diff --git a/README.md b/README.md index 9a7a7f6..5f7bc4d 100644 --- a/README.md +++ b/README.md @@ -2,29 +2,37 @@ # With WebAuthn -A repository with full stack WebAuthn API examples. +A repository with full stack **WebAuthn API** examples.
## Examples -1. **[Default WebAuthn Example - Passkeys with SimpleWebAuthn & Firebase](examples/webauthn-default/README.md)** +1. **[Authenticate with passkeys - Passkeys with SimpleWebAuthn & Firebase](examples/webauthn-default/README.md)** + - Creating (user registration), retrieving (user login), linking multiple, and removing passkeys. - - Issuing a JWT token via Firebase Auth once user is authenticated. - - Passkes are stored in Firebase Firestore. - - Formatting and parsing of WebAuthn API request / responses done via SimpleWebAuthn library. + - Passkeys autofill. + - Formatting and parsing of WebAuthn API request / responses done via [SimpleWebAuthn](https://simplewebauthn.dev) library. + - Built with [Firebase Auth](https://firebase.google.com/docs/auth/admin/create-custom-tokens) and Firestore SDKs. - 👉 [**Check out the demo**](https://with-webauthn.dev) +2. **[Upgrade to passkeys – From email/password to passkeys with SimpleWebAuthn & Firebase](examples/webauthn-upgrade/README.md)** + - A user registers with traditional email/password and verifies their email afterwards. + - Then the user can link passkey/s and therefore upgrades to MFA. + - The user can downgrade to single-factor authentication by removing all their passkeys. + - Built with [SimpleWebAuthn](https://simplewebauthn.dev), [Firebase Auth](https://firebase.google.com/docs/auth/admin/create-custom-tokens) and Firestore SDKs. + - 👉 [**Check out the demo**](https://upgrade.with-webauthn.dev) + --- ## Development ### Common Stack notes: -- The whole project is managed using tuborepo and yarn workspaces. +- The whole project is managed using tuborepo. - All examples are in NextJS (React) framework. - API calls are handled with React Tanstack query on client. -- API endpoints are build via NextJS API routes. +- API endpoints are built via NextJS API routes. - Forms are built with react-hook-form and validated with zod schemas. - Material UI with styled components as UI SDK. @@ -46,8 +54,11 @@ A repository with full stack WebAuthn API examples. yarn install --immutable ``` -3. Then continue with final steps for specific example: - - [Passkeys with SimpleWebAuthn & Firebase](examples/webauthn-default/README.md) +3. Note that common code of each example is placed in `packages/common` (for client and server). + +4. Then continue with final steps for specific example: + - [Authenticate with passkeys](examples/webauthn-default/README.md) + - [Upgrade to passkeys](examples/webauthn-upgrade/README.md) ## Have you a found a bug? diff --git a/examples/webauthn-default/README.md b/examples/webauthn-default/README.md index c546975..e4af1f4 100644 --- a/examples/webauthn-default/README.md +++ b/examples/webauthn-default/README.md @@ -1,4 +1,4 @@ -# Default WebAuthn Example - Passkeys with SimpleWebAuthn & Firebase +# Authenticate with passkeys example - Passkeys with SimpleWebAuthn & Firebase - Creating (user registration), retrieving (user login), linking multiple, and removing passkeys. - Issuing a JWT token via Firebase Auth once user is authenticated. @@ -32,4 +32,30 @@ Assuming you've already finished [those steps in the main README](../../README.m 5. Create a Firebase firestore database -2. Run `yarn dev` and checkout `http://localhost:3000` URL. + - Don't forget to set security `Rules`: + + ``` + rules_version = '2'; + + service cloud.firestore { + match /databases/{database}/documents { + // Deny all access by default + match /{document=**} { + allow read, write: if false; + } + + // Match for users collection + match /users/{uid} { + allow read: if request.auth != null && request.auth.uid == uid; + } + + // Match for passkeys collection + match /passkeys/{passkeyId} { + allow read: if request.auth != null && resource.data.userId == request.auth.uid; + } + } + } + ``` + +2. Run `yarn dev` in **root repository** and checkout `http://localhost:3000` URL. +3. Hey mate, welcome to the WebAuthn world. 🙌 diff --git a/examples/webauthn-default/next.config.ts b/examples/webauthn-default/next.config.ts index 9bf73c3..1b467eb 100755 --- a/examples/webauthn-default/next.config.ts +++ b/examples/webauthn-default/next.config.ts @@ -1,12 +1,12 @@ import type { NextConfig } from 'next'; -import { withSentryConfig } from '@sentry/nextjs'; import { config } from 'dotenv'; import type { dependencies } from 'package.json'; +import { withDefinedSentryConfig } from '@workspace/sentry/next-config'; + if (process.env.NODE_ENV === 'development') { - config({ - path: '.env.local', - }); + config({ path: '.env.local' }); + config({ path: '../../.env' }); } type Dependency = keyof typeof dependencies; @@ -28,42 +28,4 @@ const nextConfig: NextConfig = { // ensure that your source maps include changes from all other Webpack plugins export default process.env.NEXT_PUBLIC_DEV_SENTRY_DISABLED === 'true' ? nextConfig - : withSentryConfig(nextConfig, { - // For all available options, see: - // https://github.com/getsentry/sentry-webpack-plugin#options - - org: process.env.SENTRY_ORG, - project: process.env.SENTRY_PROJECT, - - // Only print logs for uploading source maps in CI - silent: !process.env.CI, - - // For all available options, see: - // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ - - // Upload a larger set of source maps for prettier stack traces (increases build time) - widenClientFileUpload: true, - - // Automatically annotate React components to show their full name in breadcrumbs and session replay - reactComponentAnnotation: { - enabled: true, - }, - - // Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. - // This can increase your server load as well as your hosting bill. - // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- - // side errors will fail. - // tunnelRoute: "/monitoring", - - // Hides source maps from generated client bundles - hideSourceMaps: true, - - // Automatically tree-shake Sentry logger statements to reduce bundle size - disableLogger: true, - - // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) - // See the following for more information: - // https://docs.sentry.io/product/crons/ - // https://vercel.com/docs/cron-jobs - automaticVercelMonitors: true, - }); + : withDefinedSentryConfig(nextConfig); diff --git a/examples/webauthn-default/package.json b/examples/webauthn-default/package.json index aa595c5..d1f3496 100644 --- a/examples/webauthn-default/package.json +++ b/examples/webauthn-default/package.json @@ -3,9 +3,10 @@ "version": "0.0.1", "type": "module", "scripts": { - "dev": "next dev --turbopack", + "types-check": "tsc --noEmit -w", + "dev": "next dev --turbopack -p 3000 & yarn types-check", "build": "next build", - "start": "next start", + "start": "next start -p 3000", "lint": "eslint-lint --config=eslint.config.mjs ./src/**/*.{ts,tsx}", "lint:fix": "yarn lint --fix", "format": "prettier-format", @@ -13,9 +14,11 @@ }, "dependencies": { "@workspace/common": "workspace:*", + "@workspace/sentry": "workspace:*", "next": "15.0.3", "react": "18.3.1", - "react-dom": "18.3.1" + "react-dom": "18.3.1", + "typescript": "5.7.2" }, "devDependencies": { "@tooling/eslint": "workspace:*", diff --git a/examples/webauthn-default/sentry.client.config.ts b/examples/webauthn-default/sentry.client.config.ts index 0ca6934..59a6448 100644 --- a/examples/webauthn-default/sentry.client.config.ts +++ b/examples/webauthn-default/sentry.client.config.ts @@ -2,29 +2,9 @@ // The config you add here will be used whenever a users loads a page in their browser. // https://docs.sentry.io/platforms/javascript/guides/nextjs/ -import * as Sentry from '@sentry/nextjs'; - import { env } from '@workspace/common/client/env'; +import { initSentryForClient } from '@workspace/sentry/client'; -if (env.NEXT_PUBLIC_DEV_SENTRY_DISABLED !== 'true') { - Sentry.init({ - dsn: env.NEXT_PUBLIC_SENTRY_DSN, - - // Add optional integrations for additional features - integrations: [Sentry.replayIntegration()], - - // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. - tracesSampleRate: 1, - - // Define how likely Replay events are sampled. - // This sets the sample rate to be 10%. You may want this to be 100% while - // in development and sample at a lower rate in production - replaysSessionSampleRate: 0.1, - - // Define how likely Replay events are sampled when an error occurs. - replaysOnErrorSampleRate: 1.0, - - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: false, - }); +if (env.NEXT_PUBLIC_DEV_SENTRY_DISABLED !== 'true' && env.NEXT_PUBLIC_SENTRY_DSN) { + initSentryForClient(env.NEXT_PUBLIC_SENTRY_DSN); } diff --git a/examples/webauthn-default/sentry.edge.config.ts b/examples/webauthn-default/sentry.edge.config.ts index ec28548..2d21a63 100644 --- a/examples/webauthn-default/sentry.edge.config.ts +++ b/examples/webauthn-default/sentry.edge.config.ts @@ -3,18 +3,9 @@ // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. // https://docs.sentry.io/platforms/javascript/guides/nextjs/ -import * as Sentry from '@sentry/nextjs'; - import { env } from '@workspace/common/client/env'; +import { initSentryForEdge } from '@workspace/sentry/edge'; -if (env.NEXT_PUBLIC_DEV_SENTRY_DISABLED !== 'true') { - Sentry.init({ - dsn: env.NEXT_PUBLIC_SENTRY_DSN, - - // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. - tracesSampleRate: 1, - - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: false, - }); +if (env.NEXT_PUBLIC_DEV_SENTRY_DISABLED !== 'true' && env.NEXT_PUBLIC_SENTRY_DSN) { + initSentryForEdge(env.NEXT_PUBLIC_SENTRY_DSN); } diff --git a/examples/webauthn-default/sentry.server.config.ts b/examples/webauthn-default/sentry.server.config.ts index e2141fa..c0ebb5f 100644 --- a/examples/webauthn-default/sentry.server.config.ts +++ b/examples/webauthn-default/sentry.server.config.ts @@ -2,18 +2,9 @@ // The config you add here will be used whenever the server handles a request. // https://docs.sentry.io/platforms/javascript/guides/nextjs/ -import * as Sentry from '@sentry/nextjs'; - import { env } from '@workspace/common/client/env'; +import { initSentryForServer } from '@workspace/sentry/server'; -if (env.NEXT_PUBLIC_DEV_SENTRY_DISABLED !== 'true') { - Sentry.init({ - dsn: env.NEXT_PUBLIC_SENTRY_DSN, - - // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. - tracesSampleRate: 1, - - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: false, - }); +if (env.NEXT_PUBLIC_DEV_SENTRY_DISABLED !== 'true' && env.NEXT_PUBLIC_SENTRY_DSN) { + initSentryForServer(env.NEXT_PUBLIC_SENTRY_DSN); } diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExample.tsx b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExample.tsx index 6b81ea0..14addfc 100644 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExample.tsx +++ b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExample.tsx @@ -2,8 +2,9 @@ import { useState } from 'react'; import { ExampleAuth, ExampleBody, ExampleFrame } from '@workspace/common/client/example/components'; -import { CurrentExampleRoute, DefaultExampleRouter } from './DefaultExampleRouter'; import { DefaultExampleTopBar } from './DefaultExampleTopBar'; +import { ResolveInitRoute } from './ResolveInitRoute'; +import { CurrentExampleRoute, ExampleRouter } from './router'; import { exampleRoutes } from './routes'; export const DefaultExample = () => { @@ -11,15 +12,17 @@ export const DefaultExample = () => { return ( - - + + + + - - + + ); }; diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExampleRouter/DefaultExampleRouter.tsx b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExampleRouter/DefaultExampleRouter.tsx deleted file mode 100644 index 3d7b6f0..0000000 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExampleRouter/DefaultExampleRouter.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { ReactNode } from 'react'; - -import { createExampleRouter, useExampleAuthSession } from '@workspace/common/client/example/components'; - -import type { ExampleRoutes } from '../routes'; - -const { ExampleRouter, useExampleRouter, CurrentExampleRoute } = createExampleRouter(); - -export interface DefaultExampleRouterProps { - children: ReactNode; - routes: ExampleRoutes; -} - -/** - * Yes, this not so smart solution but it's just an example, so please focus on the WebAutn part. Thanks. - */ -export const DefaultExampleRouter = ({ children, routes }: DefaultExampleRouterProps) => { - const { session } = useExampleAuthSession(); - - return ( - - {children} - - ); -}; - -export { CurrentExampleRoute, useExampleRouter }; diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExampleRouter/index.ts b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExampleRouter/index.ts deleted file mode 100644 index 52f6941..0000000 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExampleRouter/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './DefaultExampleRouter'; diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExampleTopBar/DefaultExampleTopBar.tsx b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExampleTopBar/DefaultExampleTopBar.tsx index caf0833..25a6cfb 100644 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExampleTopBar/DefaultExampleTopBar.tsx +++ b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExampleTopBar/DefaultExampleTopBar.tsx @@ -1,6 +1,6 @@ import { ExampleTopBar, type ExampleTopBarProps } from '@workspace/common/client/example/components'; -import { useExampleRouter } from '../DefaultExampleRouter'; +import { useExampleRouter } from '../router'; import { useExampleRouteTitle } from './hooks/useExampleRouteTitle'; export interface DefaultExampleTopBarProps extends Pick {} diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExampleTopBar/hooks/useExampleRouteTitle.ts b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExampleTopBar/hooks/useExampleRouteTitle.ts index fb8ec07..c572539 100644 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExampleTopBar/hooks/useExampleRouteTitle.ts +++ b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/DefaultExampleTopBar/hooks/useExampleRouteTitle.ts @@ -1,6 +1,6 @@ import { useExampleAuthSession } from '@workspace/common/client/example/components'; -import { useExampleRouter } from '../../DefaultExampleRouter'; +import { useExampleRouter } from '../../router'; import type { ExampleRoute } from '../../routes'; const routeTitles = { @@ -13,7 +13,7 @@ export function useExampleRouteTitle() { const { session } = useExampleAuthSession(); const { currentRoute } = useExampleRouter(); - if (session.state === 'loading') { + if (session.state === 'loading' || !currentRoute) { return 'Loading...'; } diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskey/index.ts b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskey/index.ts deleted file mode 100644 index c8bdfba..0000000 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskey/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './LoginWithPasskey'; diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskey/LoginWithPasskey.tsx b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskeyPage/LoginWithPasskeyPage.tsx similarity index 97% rename from examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskey/LoginWithPasskey.tsx rename to examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskeyPage/LoginWithPasskeyPage.tsx index e3d7c34..008be0a 100644 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskey/LoginWithPasskey.tsx +++ b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskeyPage/LoginWithPasskeyPage.tsx @@ -7,12 +7,12 @@ import { Alert, Box, Button, Divider, Words } from '@workspace/common/client/ui- import { Fingerprint, InfoOutlined } from '@workspace/common/client/ui-kit/icons'; import { AuthFormContainer } from '../AuthFormContainer'; -import { useExampleRouter } from '../DefaultExampleRouter'; +import { useExampleRouter } from '../router'; import { useConditionalMediation } from './hooks/useConditionalMediation'; import { useLoginWithPasskey } from './hooks/useLoginWithPasskey'; import { loginFormSchema, type LoginFormSchema, type LoginFormValues } from './schema'; -export const LoginWithPasskey = () => { +export const LoginWithPasskeyPage = () => { const conditionalMediation = useConditionalMediation(); const loginWithPasskey = useLoginWithPasskey(); const { redirect } = useExampleRouter(); diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskey/hooks/useConditionalMediation.ts b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskeyPage/hooks/useConditionalMediation.ts similarity index 92% rename from examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskey/hooks/useConditionalMediation.ts rename to examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskeyPage/hooks/useConditionalMediation.ts index 1ed8c90..a41dcf9 100644 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskey/hooks/useConditionalMediation.ts +++ b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskeyPage/hooks/useConditionalMediation.ts @@ -3,15 +3,15 @@ import { useQuery } from '@tanstack/react-query'; import { signInWithCustomToken } from 'firebase/auth'; import { fetcher } from '@workspace/common/client/api/fetcher'; +import { parseUnknownError } from '@workspace/common/client/errors'; import { auth } from '@workspace/common/client/firebase/config'; import { useSnack } from '@workspace/common/client/snackbar/hooks'; -import { parseWebAuthnError } from '@workspace/common/client/webauthn/utils'; import { logger } from '@workspace/common/logger'; import type { StartLoginResponseData } from '~pages/api/webauthn/login/options'; import type { VerifyLoginRequestData, VerifyLoginResponseData } from '~pages/api/webauthn/login/verify'; -import { useExampleRouter } from '../../DefaultExampleRouter'; +import { useExampleRouter } from '../../router'; export function useConditionalMediation() { const { redirect } = useExampleRouter(); @@ -61,7 +61,7 @@ export function useConditionalMediation() { return true; } catch (error) { - const parsedError = await parseWebAuthnError(error); + const parsedError = await parseUnknownError(error); if (parsedError.type !== 'ABORT_ERROR') { snack('error', parsedError.message); diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskey/hooks/useLoginWithPasskey.ts b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskeyPage/hooks/useLoginWithPasskey.ts similarity index 88% rename from examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskey/hooks/useLoginWithPasskey.ts rename to examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskeyPage/hooks/useLoginWithPasskey.ts index a615a3f..5cc2961 100644 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskey/hooks/useLoginWithPasskey.ts +++ b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/LoginWithPasskeyPage/hooks/useLoginWithPasskey.ts @@ -2,15 +2,15 @@ import { startAuthentication } from '@simplewebauthn/browser'; import { signInWithCustomToken } from 'firebase/auth'; import { fetcher } from '@workspace/common/client/api/fetcher'; +import { parseUnknownError } from '@workspace/common/client/errors'; import { auth } from '@workspace/common/client/firebase/config'; import type { FormProps } from '@workspace/common/client/form/components'; -import { parseWebAuthnError } from '@workspace/common/client/webauthn/utils'; import { logger } from '@workspace/common/logger'; import type { StartLoginRequestData, StartLoginResponseData } from '~pages/api/webauthn/login/options'; import type { VerifyLoginRequestData, VerifyLoginResponseData } from '~pages/api/webauthn/login/verify'; -import { useExampleRouter } from '../../DefaultExampleRouter'; +import { useExampleRouter } from '../../router'; import type { LoginFormSchema, LoginFormValues } from '../schema'; export function useLoginWithPasskey(): FormProps['onSubmit'] { @@ -32,7 +32,7 @@ export function useLoginWithPasskey(): FormProps({ method: 'POST', @@ -50,7 +50,7 @@ export function useLoginWithPasskey(): FormProps ({ - marginBottom: theme.spacing(2), - marginTop: theme.spacing(2), - display: 'grid', - justifyContent: 'space-between', - alignItems: 'center', - gridTemplateColumns: 'auto auto', -})); - -export const PasskeysList = styled('section')(({ theme }) => ({ - display: 'grid', - rowGap: theme.spacing(3.5), -})); diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/Passkeys.tsx b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/Passkeys.tsx deleted file mode 100644 index bafdbfa..0000000 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/Passkeys.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { QueryError, QueryLoader } from '@workspace/common/client/api/components'; -import { useDialog } from '@workspace/common/client/dialog/hooks'; -import { Passkey } from '@workspace/common/client/passkeys/components'; -import { Button, Words } from '@workspace/common/client/ui-kit'; -import { Fingerprint } from '@workspace/common/client/ui-kit/icons'; - -import { useAddPasskey } from './hooks/useAddPasskey'; -import { useFetchPasskeys } from './hooks/useFetchPasskeys'; -import { useRemovePasskey } from './hooks/useRemovePasskey'; -import { PasskeysHeader, PasskeysList } from './Passkeys.styles'; -import { PostRemovalDialog, type PostRemovalDialogProps } from './PostRemovalDialog'; - -export const Passkeys = () => { - const passkeysResult = useFetchPasskeys(); - const addPasskey = useAddPasskey(); - const postRemovalDialog = useDialog(); - const removePasskey = useRemovePasskey(postRemovalDialog.openDialog); - - return ( - <> - - Passkeys - - - - - - {passkeysResult.data.map(passkey => ( - 1} - removePasskey={removePasskey} - /> - ))} - - - - - - ); -}; diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/hooks/useFetchPasskeys.ts b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/hooks/useFetchPasskeys.ts deleted file mode 100644 index b7d7d1d..0000000 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/hooks/useFetchPasskeys.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -import { useAuthUser } from '@workspace/common/client/example/components'; -import { fetchUserPasskeys } from '@workspace/common/client/firebase/services/passkeys'; - -export function useFetchPasskeys() { - const authUser = useAuthUser(); - const uid = authUser!.uid; - - return useQuery({ - queryKey: ['passkeys', uid], - queryFn: () => fetchUserPasskeys(uid), - initialData: [], - }); -} diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/index.ts b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/index.ts deleted file mode 100644 index c16da17..0000000 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Passkeys'; diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/PasskeysPage/PasskeysPage.tsx b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/PasskeysPage/PasskeysPage.tsx new file mode 100644 index 0000000..2471f9c --- /dev/null +++ b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/PasskeysPage/PasskeysPage.tsx @@ -0,0 +1,28 @@ +import { useDialog } from '@workspace/common/client/dialog/hooks'; +import { + PasskeysHeader, + PasskeysList, + PostRemovalDialog, + type PostRemovalDialogProps, +} from '@workspace/common/client/passkeys/components'; + +import { useAddPasskey } from './hooks/useAddPasskey'; +import { useRemovePasskey } from './hooks/useRemovePasskey'; + +export const PasskeysPage = () => { + const addPasskey = useAddPasskey(); + const postRemovalDialog = useDialog(); + const removePasskey = useRemovePasskey(postRemovalDialog.openDialog); + + return ( + <> + + + + + ); +}; diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/hooks/useAddPasskey.ts b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/PasskeysPage/hooks/useAddPasskey.ts similarity index 85% rename from examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/hooks/useAddPasskey.ts rename to examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/PasskeysPage/hooks/useAddPasskey.ts index 8356a96..b686615 100644 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/hooks/useAddPasskey.ts +++ b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/PasskeysPage/hooks/useAddPasskey.ts @@ -3,6 +3,7 @@ import { useMutation } from '@tanstack/react-query'; import { queryClient } from '@workspace/common/client/api/components'; import { fetcher } from '@workspace/common/client/api/fetcher'; +import { parseUnknownError } from '@workspace/common/client/errors'; import { useSnack } from '@workspace/common/client/snackbar/hooks'; import { logger } from '@workspace/common/logger'; @@ -40,8 +41,12 @@ export function useAddPasskey() { queryKey: ['passkeys'], }); }, - onError(error: Error) { - snack('error', error.message); + async onError(error: Error) { + const parsedError = await parseUnknownError(error); + + logger.error(parsedError); + + snack('error', parsedError.message); }, onSuccess() { snack('success', 'Passkey has been successfully added.'); diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/hooks/useRemovePasskey.ts b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/PasskeysPage/hooks/useRemovePasskey.ts similarity index 77% rename from examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/hooks/useRemovePasskey.ts rename to examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/PasskeysPage/hooks/useRemovePasskey.ts index 81feda2..e1326de 100644 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/hooks/useRemovePasskey.ts +++ b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/PasskeysPage/hooks/useRemovePasskey.ts @@ -3,15 +3,14 @@ import { useMutation } from '@tanstack/react-query'; import { queryClient } from '@workspace/common/client/api/components'; import { fetcher } from '@workspace/common/client/api/fetcher'; +import { parseUnknownError } from '@workspace/common/client/errors'; import { useAuthUser } from '@workspace/common/client/example/components'; +import type { PostRemovalDialogProps } from '@workspace/common/client/passkeys/components'; import { useSnack } from '@workspace/common/client/snackbar/hooks'; import { logger } from '@workspace/common/logger'; -import type { Passkey } from '@workspace/common/types'; import type { StartRemovalResponseData } from '~pages/api/webauthn/remove/options'; -import type { VerifyRemovalRequestData } from '~pages/api/webauthn/remove/verify'; - -import type { PostRemovalDialogProps } from '../PostRemovalDialog'; +import type { VerifyRemovalRequestData, VerifyRemovalResponseData } from '~pages/api/webauthn/remove/verify'; /** * Remove a passkey from the user's account. User must verify their identity before removing the passkey. @@ -36,9 +35,11 @@ export function useRemovePasskey(openDialog: (data: PostRemovalDialogProps['data optionsJSON: publicKeyOptions, }); - logger.info('Authentication result:', result); + logger.info('WebAuthn API result:', result); - const { data: passkey } = await fetcher({ + const { + data: { passkey }, + } = await fetcher({ method: 'POST', url: '/webauthn/remove/verify', body: { @@ -59,7 +60,11 @@ export function useRemovePasskey(openDialog: (data: PostRemovalDialogProps['data queryKey: ['passkeys'], }); } catch (error) { - snack('error', (error as Error).message); + const parsedError = await parseUnknownError(error); + + snack('error', parsedError.message); + + logger.error(error); } }, }); diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/PasskeysPage/index.ts b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/PasskeysPage/index.ts new file mode 100644 index 0000000..1d1082a --- /dev/null +++ b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/PasskeysPage/index.ts @@ -0,0 +1 @@ +export * from './PasskeysPage'; diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskey/index.ts b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskey/index.ts deleted file mode 100644 index 2b614c0..0000000 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskey/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './RegisterWithPasskey'; diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskey/RegisterWithPasskey.tsx b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskeyPage/RegisterWithPasskeyPage.tsx similarity index 94% rename from examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskey/RegisterWithPasskey.tsx rename to examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskeyPage/RegisterWithPasskeyPage.tsx index ab3df46..5e4ded8 100644 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskey/RegisterWithPasskey.tsx +++ b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskeyPage/RegisterWithPasskeyPage.tsx @@ -3,11 +3,11 @@ import { Box, Button, Divider, Words } from '@workspace/common/client/ui-kit'; import { Fingerprint } from '@workspace/common/client/ui-kit/icons'; import { AuthFormContainer } from '../AuthFormContainer'; -import { useExampleRouter } from '../DefaultExampleRouter'; +import { useExampleRouter } from '../router'; import { useRegisterWithPasskey } from './hooks'; import { registerFormSchema, type RegisterFormSchema, type RegisterFormValues } from './schema'; -export const RegisterWithPasskey = () => { +export const RegisterWithPasskeyPage = () => { const registerPasskey = useRegisterWithPasskey(); const { redirect } = useExampleRouter(); diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskey/hooks/index.ts b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskeyPage/hooks/index.ts similarity index 100% rename from examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskey/hooks/index.ts rename to examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskeyPage/hooks/index.ts diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskey/hooks/useRegisterWithPasskey.ts b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskeyPage/hooks/useRegisterWithPasskey.ts similarity index 91% rename from examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskey/hooks/useRegisterWithPasskey.ts rename to examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskeyPage/hooks/useRegisterWithPasskey.ts index a0acd4d..25ce1f2 100644 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskey/hooks/useRegisterWithPasskey.ts +++ b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/RegisterWithPasskeyPage/hooks/useRegisterWithPasskey.ts @@ -2,9 +2,9 @@ import { startRegistration } from '@simplewebauthn/browser'; import { signInWithCustomToken } from 'firebase/auth'; import { fetcher } from '@workspace/common/client/api/fetcher'; +import { parseUnknownError } from '@workspace/common/client/errors'; import { auth } from '@workspace/common/client/firebase/config'; import type { FormProps } from '@workspace/common/client/form/components'; -import { parseWebAuthnError } from '@workspace/common/client/webauthn/utils'; import { logger } from '@workspace/common/logger'; import type { StartRegistrationRequestData, StartRegistrationResponseData } from '~pages/api/webauthn/register/options'; @@ -13,7 +13,7 @@ import type { VerifyRegistrationResponseData, } from '~pages/api/webauthn/register/verify'; -import { useExampleRouter } from '../../DefaultExampleRouter'; +import { useExampleRouter } from '../../router'; import type { RegisterFormSchema, RegisterFormValues } from '../schema'; export function useRegisterWithPasskey(): FormProps['onSubmit'] { @@ -53,7 +53,7 @@ export function useRegisterWithPasskey(): FormProps { + const { currentRoute, redirect } = useExampleRouter(); + const { session } = useExampleAuthSession(); + + useEffect(() => { + if (currentRoute !== null) { + return; + } + + switch (session.state) { + case 'authenticated': + redirect('/passkeys'); + break; + case 'unauthenticated': + redirect('/register'); + break; + } + }, [session, currentRoute, redirect]); + + return null; +}; diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/ResolveInitRoute/index.ts b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/ResolveInitRoute/index.ts new file mode 100644 index 0000000..06c3fba --- /dev/null +++ b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/ResolveInitRoute/index.ts @@ -0,0 +1 @@ +export * from './ResolveInitRoute'; diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/router/index.ts b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/router/index.ts new file mode 100644 index 0000000..58d5b70 --- /dev/null +++ b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/router/index.ts @@ -0,0 +1,7 @@ +import { createExampleRouter } from '@workspace/common/client/example/components'; + +import type { ExampleRoutes } from '../routes'; + +const { ExampleRouter, useExampleRouter, CurrentExampleRoute } = createExampleRouter(); + +export { CurrentExampleRoute, ExampleRouter, useExampleRouter }; diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/routes/index.tsx b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/routes/index.tsx index fca83ab..81492b1 100644 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/routes/index.tsx +++ b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/routes/index.tsx @@ -1,13 +1,13 @@ import type { UnknownRoutes } from '@workspace/common/client/example/components'; -import { LoginWithPasskey } from '../LoginWithPasskey'; -import { Passkeys } from '../Passkeys'; -import { RegisterWithPasskey } from '../RegisterWithPasskey'; +import { LoginWithPasskeyPage } from '../LoginWithPasskeyPage'; +import { PasskeysPage } from '../PasskeysPage'; +import { RegisterWithPasskeyPage } from '../RegisterWithPasskeyPage'; export const exampleRoutes = { - '/register': () => , - '/login': () => , - '/passkeys': () => , + '/register': RegisterWithPasskeyPage, + '/login': LoginWithPasskeyPage, + '/passkeys': PasskeysPage, } as const satisfies UnknownRoutes; export type ExampleRoutes = typeof exampleRoutes; diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/WebAuthnDefaultExamplePage.tsx b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/WebAuthnDefaultExamplePage.tsx index 40b32de..6052c51 100644 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/WebAuthnDefaultExamplePage.tsx +++ b/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/WebAuthnDefaultExamplePage.tsx @@ -1,7 +1,7 @@ import Link from 'next/link'; import { ExampleDescription, ExampleHeader, ExampleWrapper } from '@workspace/common/client/example/components'; -import { MainHeader } from '@workspace/common/client/layout/components'; +import { PageHeader } from '@workspace/common/client/layout/components'; import { Container } from '@workspace/common/client/ui-kit'; import { DefaultExample } from './DefaultExample'; @@ -9,7 +9,7 @@ import { DefaultExample } from './DefaultExample'; export const WebAuthnDefaultExamplePage = () => { return ( <> - + @@ -32,6 +32,7 @@ export const WebAuthnDefaultExamplePage = () => { ]} /> } + githubUrl='https://github.com/cermakjiri/with-webauthn/tree/dev/examples/webauthn-default' /> diff --git a/examples/webauthn-default/src/pages/api/webauthn/login/options.ts b/examples/webauthn-default/src/pages/api/webauthn/login/options.ts index ebcd319..f969c0d 100644 --- a/examples/webauthn-default/src/pages/api/webauthn/login/options.ts +++ b/examples/webauthn-default/src/pages/api/webauthn/login/options.ts @@ -29,9 +29,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< const passkeys = await getUserPasskeys(body?.username); - /** - * Generate a random string with enough entropy to prevent replay attacks. - */ + // Generate a random string with enough entropy to prevent replay attacks. const challenge = await generateChallenge(); const authenticationOptions = await generateAuthenticationOptions({ diff --git a/examples/webauthn-default/src/pages/api/webauthn/login/verify.ts b/examples/webauthn-default/src/pages/api/webauthn/login/verify.ts index c7bde83..e787157 100644 --- a/examples/webauthn-default/src/pages/api/webauthn/login/verify.ts +++ b/examples/webauthn-default/src/pages/api/webauthn/login/verify.ts @@ -6,7 +6,7 @@ import { FieldValue } from 'firebase-admin/firestore'; import { env } from '@workspace/common/client/env'; import { logger } from '@workspace/common/logger'; -import { createCustomToken } from '@workspace/common/server/services/auth'; +import { auth } from '@workspace/common/server/config/firebase'; import { retrieveAndInvalidateChallengeSession } from '@workspace/common/server/services/challenge-session'; import { getPasskeyBy, updatePasskey } from '@workspace/common/server/services/passkeys'; @@ -70,7 +70,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< lastUsedAt: FieldValue.serverTimestamp(), }); - const customToken = await createCustomToken(passkey.userId); + const customToken = await auth().createCustomToken(passkey.userId); res.status(200).json({ customToken }); } catch (error) { diff --git a/examples/webauthn-default/src/pages/api/webauthn/register/verify.ts b/examples/webauthn-default/src/pages/api/webauthn/register/verify.ts index 9bdadec..7ddfa46 100644 --- a/examples/webauthn-default/src/pages/api/webauthn/register/verify.ts +++ b/examples/webauthn-default/src/pages/api/webauthn/register/verify.ts @@ -4,7 +4,7 @@ import type { RegistrationResponseJSON } from '@simplewebauthn/types'; import { env } from '@workspace/common/client/env'; import { logger } from '@workspace/common/logger'; -import { createCustomToken } from '@workspace/common/server/services/auth'; +import { auth } from '@workspace/common/server/config/firebase'; import { retrieveAndInvalidateChallengeSession } from '@workspace/common/server/services/challenge-session'; import { createUserPasskey, findUserByUsername } from '@workspace/common/server/services/users'; @@ -56,7 +56,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< * Creates a new Firebase custom token (JWT) * that can be sent back to a client device to use to sign in with the client SDKs' signInWithCustomToken() methods. */ - const customToken = await createCustomToken(userId); + const customToken = await auth().createCustomToken(userId); res.status(200).json({ customToken }); } catch (error) { diff --git a/examples/webauthn-default/src/pages/api/webauthn/remove/options.ts b/examples/webauthn-default/src/pages/api/webauthn/remove/options.ts index ea71de1..36b96d5 100644 --- a/examples/webauthn-default/src/pages/api/webauthn/remove/options.ts +++ b/examples/webauthn-default/src/pages/api/webauthn/remove/options.ts @@ -27,9 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const userId = idTokenResult.uid; const passkeys = await getPasskeys(userId); - /** - * Generate a random string with enough entropy to prevent replay attacks. - */ + // Generate a random string with enough entropy to prevent replay attacks. const challenge = await generateChallenge(); const authenticationOptions = await generateAuthenticationOptions({ diff --git a/examples/webauthn-default/src/pages/api/webauthn/remove/verify.ts b/examples/webauthn-default/src/pages/api/webauthn/remove/verify.ts index dea17fa..5ff623d 100644 --- a/examples/webauthn-default/src/pages/api/webauthn/remove/verify.ts +++ b/examples/webauthn-default/src/pages/api/webauthn/remove/verify.ts @@ -16,10 +16,17 @@ export type VerifyRemovalRequestData = { passkeyId: string; }; +export type VerifyRemovalResponseData = { + /** + * Removed passkey. + */ + passkey: Passkey; +}; + /** * Verify the user's identity before removing the passkey. */ -export default async function handler(req: NextApiRequest, res: NextApiResponse) { +export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { const idTokenResult = await parseAndVerifyIdToken(req.headers.authorization); @@ -71,7 +78,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< await removeUserPasskey(passkeyForRemoval.userId, passkeyForRemoval.id); - res.status(200).json(passkeyForRemoval); + res.status(200).json({ passkey: passkeyForRemoval }); } catch (error) { logger.error(error); diff --git a/examples/webauthn-upgrade/.env.template.local b/examples/webauthn-upgrade/.env.template.local new file mode 100644 index 0000000..8f3f5a8 --- /dev/null +++ b/examples/webauthn-upgrade/.env.template.local @@ -0,0 +1,20 @@ +# Next.js +NEXT_TELEMETRY_DISABLED=1 + +NEXT_PUBLIC_CLIENT_ORIGIN=http://localhost:3000 +# NEXT_PUBLIC_CLIENT_ORIGIN=https://14e5-2a07-b242-101a-9700-15d2-5b2c-c72c-ab56.ngrok-free.app + +NEXT_PUBLIC_FIREBASE_API_KEY="" +NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="" +NEXT_PUBLIC_FIREBASE_PROJECT_ID= "" +NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="" +NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="" +NEXT_PUBLIC_FIREBASE_APP_ID="" +NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID="" +NEXT_PUBLIC_FIREBASE_DB_ID="" + +# Sentry +NEXT_PUBLIC_DEV_SENTRY_DISABLED=true +# NEXT_PUBLIC_SENTRY_DSN="" +# SENTRY_ORG="" +# SENTRY_PROJECT="" \ No newline at end of file diff --git a/examples/webauthn-upgrade/.env.template.server b/examples/webauthn-upgrade/.env.template.server new file mode 100644 index 0000000..74ac659 --- /dev/null +++ b/examples/webauthn-upgrade/.env.template.server @@ -0,0 +1,4 @@ +FIREBASE_PROJECT_ID="" +FIREBASE_PRIVATE_KEY="" +FIREBASE_CLIENT_EMAIL="" +FIREBASE_DB_ID="" \ No newline at end of file diff --git a/examples/webauthn-upgrade/.gitignore b/examples/webauthn-upgrade/.gitignore new file mode 100755 index 0000000..b9e3329 --- /dev/null +++ b/examples/webauthn-upgrade/.gitignore @@ -0,0 +1,52 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +.pnp +.pnp.js + +# testing +coverage + +# storybook +dist/ + +# next.js +.next +out +build-storybook.log + +# production +build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# vercel +.vercel + +# typescript +*.tsbuildinfo + +# Sentry +.sentryclirc + +!src/translations/*.mock.json + +.turbo + +node_modules + +# Sentry Config File +.env.sentry-build-plugin + +public/firebase-messaging-sw.* + +.env.server.jsonc \ No newline at end of file diff --git a/examples/webauthn-upgrade/.madgerc b/examples/webauthn-upgrade/.madgerc new file mode 100644 index 0000000..53eda8b --- /dev/null +++ b/examples/webauthn-upgrade/.madgerc @@ -0,0 +1,15 @@ +{ + "fileExtensions": [ + "ts", + "tsx" + ], + "tsConfig": "./tsconfig.json", + "detectiveOptions": { + "ts": { + "skipTypeImports": true + }, + "tsx": { + "skipTypeImports": true + } + } +} diff --git a/examples/webauthn-upgrade/README.md b/examples/webauthn-upgrade/README.md new file mode 100644 index 0000000..bc239d9 --- /dev/null +++ b/examples/webauthn-upgrade/README.md @@ -0,0 +1,78 @@ +# Upgrade to passkeys example - From email/password to passkeys + +- A user registers with traditional email/password and verifies their email afterwards. +- Then the user can link passkey/s and therefore upgrades to MFA. +- The user can downgrade to single-factor authentication by removing all their passkeys. +- Built with [SimpleWebAuthn](https://simplewebauthn.dev), [Firebase Auth](https://firebase.google.com/docs/auth/admin/create-custom-tokens) and Firestore SDKs. + +A part from that, the demo includes: + +- Creating (user registration), retrieving (user login), linking multiple, and removing passkeys. +- Issuing a JWT token via Firebase Auth once user is authenticated. +- Passkes are stored in Firebase Firestore. +- Formatting and parsing of WebAuthn API request / responses done via SimpleWebAuthn library. + +👉 **[Check out the demo](https://upgrade.with-webauthn.dev)**. + +## Development + +### How to start it locally? + +Assuming you've already finished [those steps in the main README](../../README.md), let's proceed: + +> Note you can use the same Firebase project for multiple examples. +> However, each example requires its own Firestore database, see details below. + +1. Setup Firebase: + + 1. Create a new Firebase project + + 2. Initialize `Authentication` + - Enable `Email/Password` sign-in provider. + - Add `localhost` as Authorized domain in Firebase: + - `Firebase > Authentication > Settings > Authorised domains.` + - Set Email verification URL: + 1. `Firebase > Authentication > Templates > Email address verification` + 2. `Customize action URL` + 3. `http://localhost:3001` + 3. Create a Firebase firestore database + + - Initialize a new database. + - Set security rules: + + ``` + rules_version = '2'; + + service cloud.firestore { + match /databases/{database}/documents { + // Deny all access by default + match /{document=**} { + allow read, write: if false; + } + + // Match for users collection + match /users/{uid} { + allow read: if request.auth != null && request.auth.uid == uid && request.auth.token.email_verified == true; + } + + // Match for passkeys collection + match /passkeys/{passkeyId} { + allow read: if request.auth != null && resource.data.userId == request.auth.uid && request.auth.token.email_verified == true && request.auth.token.mfa_enabled == true; + } + } + } + ``` + + 4. Copy `.env.template.local` to `.env.local`: + + - Fill up all those `NEXT_PUBLIC_FIREBASE_` env. vars. + - Don't forget to set `NEXT_PUBLIC_FIREBASE_DB_ID=firestore-db-name`. + + 5. Copy `.env.template.server` to `.env.server`: + + - Create a new private key in `Firebase > Project settings > Service accounts`. + - Fill up all those `FIREBASE_` env. vars. + - Don't forget to set `FIREBASE_DB_ID=firestore-db-name`. + +2. Run `yarn dev` in **root repository** and checkout `http://localhost:3001` URL. +3. Hey mate, welcome to the WebAuthn world. 🙌 diff --git a/examples/webauthn-upgrade/eslint.config.mjs b/examples/webauthn-upgrade/eslint.config.mjs new file mode 100644 index 0000000..5355fa5 --- /dev/null +++ b/examples/webauthn-upgrade/eslint.config.mjs @@ -0,0 +1,3 @@ +import { nextjsConfig } from '@tooling/eslint/config'; + +export default nextjsConfig; diff --git a/examples/webauthn-upgrade/next-env.d.ts b/examples/webauthn-upgrade/next-env.d.ts new file mode 100644 index 0000000..a4a7b3f --- /dev/null +++ b/examples/webauthn-upgrade/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/webauthn-upgrade/next.config.ts b/examples/webauthn-upgrade/next.config.ts new file mode 100755 index 0000000..1b467eb --- /dev/null +++ b/examples/webauthn-upgrade/next.config.ts @@ -0,0 +1,31 @@ +import type { NextConfig } from 'next'; +import { config } from 'dotenv'; +import type { dependencies } from 'package.json'; + +import { withDefinedSentryConfig } from '@workspace/sentry/next-config'; + +if (process.env.NODE_ENV === 'development') { + config({ path: '.env.local' }); + config({ path: '../../.env' }); +} + +type Dependency = keyof typeof dependencies; + +const nextConfig: NextConfig = { + reactStrictMode: true, + + i18n: { + locales: ['en'], + defaultLocale: 'en', + }, + + transpilePackages: ['@workspace/common'] satisfies Dependency[], + + redirects: async () => [], +}; + +// Make sure adding Sentry options is the last code to run before exporting, to +// ensure that your source maps include changes from all other Webpack plugins +export default process.env.NEXT_PUBLIC_DEV_SENTRY_DISABLED === 'true' + ? nextConfig + : withDefinedSentryConfig(nextConfig); diff --git a/examples/webauthn-upgrade/package.json b/examples/webauthn-upgrade/package.json new file mode 100644 index 0000000..62ee11f --- /dev/null +++ b/examples/webauthn-upgrade/package.json @@ -0,0 +1,34 @@ +{ + "name": "webauthn-upgrade-example", + "version": "0.0.1", + "type": "module", + "scripts": { + "types-check": "tsc --noEmit -w", + "dev": "next dev --turbopack -p 3001 & yarn types-check", + "build": "next build", + "start": "next start -p 3001", + "lint": "eslint-lint --config=eslint.config.mjs ./src/**/*.{ts,tsx}", + "lint:fix": "yarn lint --fix", + "format": "prettier-format", + "cir-dep": "check-cir-deps ./src" + }, + "dependencies": { + "@workspace/common": "workspace:*", + "@workspace/sentry": "workspace:*", + "next": "15.0.3", + "react": "18.3.1", + "react-dom": "18.3.1", + "typescript": "5.7.2" + }, + "devDependencies": { + "@tooling/eslint": "workspace:*", + "@tooling/madge": "workspace:*", + "@tooling/prettier": "workspace:*", + "@tooling/typescript": "workspace:*", + "browserslist-config-custom": "workspace:*" + }, + "prettier": "@tooling/prettier/config", + "browserslist": [ + "extends browserslist-config-custom" + ] +} diff --git a/examples/webauthn-upgrade/public/manifest.json b/examples/webauthn-upgrade/public/manifest.json new file mode 100644 index 0000000..2af852b --- /dev/null +++ b/examples/webauthn-upgrade/public/manifest.json @@ -0,0 +1,12 @@ +{ + "theme_color": "#000000", + "background_color": "#ffffff", + "icons": [], + "orientation": "portrait", + "display": "standalone", + "dir": "auto", + "lang": "en-US", + "name": "With WebAuthn full stack example", + "short_name": "With WebAuthn", + "start_url": "/" +} diff --git a/examples/webauthn-upgrade/sentry.client.config.ts b/examples/webauthn-upgrade/sentry.client.config.ts new file mode 100644 index 0000000..59a6448 --- /dev/null +++ b/examples/webauthn-upgrade/sentry.client.config.ts @@ -0,0 +1,10 @@ +// This file configures the initialization of Sentry on the client. +// The config you add here will be used whenever a users loads a page in their browser. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import { env } from '@workspace/common/client/env'; +import { initSentryForClient } from '@workspace/sentry/client'; + +if (env.NEXT_PUBLIC_DEV_SENTRY_DISABLED !== 'true' && env.NEXT_PUBLIC_SENTRY_DSN) { + initSentryForClient(env.NEXT_PUBLIC_SENTRY_DSN); +} diff --git a/examples/webauthn-upgrade/sentry.edge.config.ts b/examples/webauthn-upgrade/sentry.edge.config.ts new file mode 100644 index 0000000..2d21a63 --- /dev/null +++ b/examples/webauthn-upgrade/sentry.edge.config.ts @@ -0,0 +1,11 @@ +// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). +// The config you add here will be used whenever one of the edge features is loaded. +// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import { env } from '@workspace/common/client/env'; +import { initSentryForEdge } from '@workspace/sentry/edge'; + +if (env.NEXT_PUBLIC_DEV_SENTRY_DISABLED !== 'true' && env.NEXT_PUBLIC_SENTRY_DSN) { + initSentryForEdge(env.NEXT_PUBLIC_SENTRY_DSN); +} diff --git a/examples/webauthn-upgrade/sentry.server.config.ts b/examples/webauthn-upgrade/sentry.server.config.ts new file mode 100644 index 0000000..c0ebb5f --- /dev/null +++ b/examples/webauthn-upgrade/sentry.server.config.ts @@ -0,0 +1,10 @@ +// This file configures the initialization of Sentry on the server. +// The config you add here will be used whenever the server handles a request. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import { env } from '@workspace/common/client/env'; +import { initSentryForServer } from '@workspace/sentry/server'; + +if (env.NEXT_PUBLIC_DEV_SENTRY_DISABLED !== 'true' && env.NEXT_PUBLIC_SENTRY_DSN) { + initSentryForServer(env.NEXT_PUBLIC_SENTRY_DSN); +} diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/AuthFormContainer/AuthFormContainer.styles.tsx b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/AuthFormContainer/AuthFormContainer.styles.tsx new file mode 100644 index 0000000..c8f7dc8 --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/AuthFormContainer/AuthFormContainer.styles.tsx @@ -0,0 +1,10 @@ +import { styled } from '@workspace/common/client/ui-kit'; + +export const AuthFormContainer = styled('div')(() => ({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + height: '100%', + maxWidth: 400, + margin: 'auto', +})); diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/AuthFormContainer/index.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/AuthFormContainer/index.ts new file mode 100644 index 0000000..ab6fa1d --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/AuthFormContainer/index.ts @@ -0,0 +1 @@ +export * from './AuthFormContainer.styles'; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/DefaultExampleTopBar/DefaultExampleTopBar.tsx b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/DefaultExampleTopBar/DefaultExampleTopBar.tsx new file mode 100644 index 0000000..25a6cfb --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/DefaultExampleTopBar/DefaultExampleTopBar.tsx @@ -0,0 +1,22 @@ +import { ExampleTopBar, type ExampleTopBarProps } from '@workspace/common/client/example/components'; + +import { useExampleRouter } from '../router'; +import { useExampleRouteTitle } from './hooks/useExampleRouteTitle'; + +export interface DefaultExampleTopBarProps extends Pick {} + +export const DefaultExampleTopBar = ({ expanded, onToggleExpand }: DefaultExampleTopBarProps) => { + const title = useExampleRouteTitle(); + const { redirect } = useExampleRouter(); + + return ( + { + redirect('/login'); + }} + /> + ); +}; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/DefaultExampleTopBar/hooks/useExampleRouteTitle.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/DefaultExampleTopBar/hooks/useExampleRouteTitle.ts new file mode 100644 index 0000000..379c04f --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/DefaultExampleTopBar/hooks/useExampleRouteTitle.ts @@ -0,0 +1,22 @@ +import { useExampleAuthSession } from '@workspace/common/client/example/components'; + +import { useExampleRouter } from '../../router'; +import type { ExampleRoute } from '../../routes'; + +const routeTitles = { + '/passkeys': 'Registered passkeys', + '/register': 'Demo Registration', + '/login': 'Demo Login', + '/login-with-password': 'Demo Login', +} as const satisfies Record; + +export function useExampleRouteTitle() { + const { session } = useExampleAuthSession(); + const { currentRoute } = useExampleRouter(); + + if (session.state === 'loading' || !currentRoute) { + return 'Loading...'; + } + + return routeTitles[currentRoute]; +} diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/DefaultExampleTopBar/index.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/DefaultExampleTopBar/index.ts new file mode 100644 index 0000000..6670f03 --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/DefaultExampleTopBar/index.ts @@ -0,0 +1 @@ +export * from './DefaultExampleTopBar'; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/EmailVerificationCode/EmailVerificationCode.tsx b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/EmailVerificationCode/EmailVerificationCode.tsx new file mode 100644 index 0000000..9acad16 --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/EmailVerificationCode/EmailVerificationCode.tsx @@ -0,0 +1,51 @@ +import type { ReactNode } from 'react'; +import { useRouter } from 'next/router'; +import { useQuery } from '@tanstack/react-query'; +import { applyActionCode } from 'firebase/auth'; +import { parseAsString, parseAsStringLiteral, useQueryStates } from 'nuqs'; + +import { QueryLoader } from '@workspace/common/client/api/components'; +import { env } from '@workspace/common/client/env'; +import { auth } from '@workspace/common/client/firebase/config'; + +import { useExampleRouter } from '../router'; + +export interface EmailVerificationCodeProps { + children: ReactNode; +} + +/** + * Parse the email verification code from the query parameters and attempt to verify the email. + */ +export function EmailVerificationCode({ children }: EmailVerificationCodeProps) { + const [params] = useQueryStates({ + apiKey: parseAsString, + mode: parseAsStringLiteral(['verifyEmail'] as const), + oobCode: parseAsString, + continueUrl: parseAsString, + }); + + const { push } = useRouter(); + const { redirect } = useExampleRouter(); + + const result = useQuery({ + queryKey: ['emailVerification', params], + queryFn: async () => { + const { mode, oobCode, continueUrl, apiKey } = params; + + if (mode !== 'verifyEmail' || !oobCode || apiKey !== env.NEXT_PUBLIC_FIREBASE_API_KEY || !continueUrl) { + return null; + } + + await applyActionCode(auth(), oobCode); + + redirect('/login-with-password'); + + await push(continueUrl); + + return null; + }, + }); + + return {children}; +} diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/EmailVerificationCode/index.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/EmailVerificationCode/index.ts new file mode 100644 index 0000000..9df64dc --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/EmailVerificationCode/index.ts @@ -0,0 +1 @@ +export * from './EmailVerificationCode'; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/EmailVerifiedAlert/EmailVerifiedAlert.tsx b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/EmailVerifiedAlert/EmailVerifiedAlert.tsx new file mode 100644 index 0000000..0f15b0a --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/EmailVerifiedAlert/EmailVerifiedAlert.tsx @@ -0,0 +1,32 @@ +import { parseAsBoolean, useQueryState } from 'nuqs'; + +import { Alert, Divider, Words } from '@workspace/common/client/ui-kit'; + +export interface EmailVerifiedAlertProps {} + +export const EmailVerifiedAlert = ({}: EmailVerifiedAlertProps) => { + const [verified, setVerified] = useQueryState('verified', parseAsBoolean); + + if (verified !== true) { + return null; + } + + return ( + <> + { + setVerified(null); + }} + sx={{ mb: 3 }} + > + + Email verified. +
+ You can now add passkey to your account. +
+
+ + + ); +}; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/EmailVerifiedAlert/index.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/EmailVerifiedAlert/index.ts new file mode 100644 index 0000000..3ae170c --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/EmailVerifiedAlert/index.ts @@ -0,0 +1 @@ +export * from './EmailVerifiedAlert'; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithEmailAndPasswordPage/LoginWithEmailAndPasswordPage.tsx b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithEmailAndPasswordPage/LoginWithEmailAndPasswordPage.tsx new file mode 100644 index 0000000..cfa2c2d --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithEmailAndPasswordPage/LoginWithEmailAndPasswordPage.tsx @@ -0,0 +1,54 @@ +import { useQueryState } from 'nuqs'; + +import { + EmailField, + FieldsStack, + Form, + FormError, + PasswordField, + SubmitButton, +} from '@workspace/common/client/form/components'; +import { Button } from '@workspace/common/client/ui-kit'; +import { ArrowBack, Send } from '@workspace/common/client/ui-kit/icons'; + +import { AuthFormContainer } from '../AuthFormContainer'; +import { useExampleRouter } from '../router'; +import { useLoginWithEmailAndPassword } from './hooks/useLoginWithEmailAndPassword'; +import { loginFormSchema, type LoginFormSchema, type LoginFormValues } from './schema'; + +export const LoginWithEmailAndPasswordPage = () => { + const [email, setEmail] = useQueryState('email'); + const login = useLoginWithEmailAndPassword({ + onSuccess() { + setEmail(null); + }, + }); + + const { redirect } = useExampleRouter(); + + return ( + <> + + + + schema={loginFormSchema} + onSubmit={login} + mode='onTouched' + defaultValues={email ? { email } : {}} + > + + + name='email' autoComplete='email' /> + name='password' autoComplete='current-password' /> + + + }> + Login + + + + + ); +}; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithEmailAndPasswordPage/hooks/useLoginWithEmailAndPassword.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithEmailAndPasswordPage/hooks/useLoginWithEmailAndPassword.ts new file mode 100644 index 0000000..05f9b68 --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithEmailAndPasswordPage/hooks/useLoginWithEmailAndPassword.ts @@ -0,0 +1,74 @@ +import { startAuthentication } from '@simplewebauthn/browser'; +import { signInWithCustomToken } from 'firebase/auth'; + +import { fetcher } from '@workspace/common/client/api/fetcher'; +import { parseUnknownError } from '@workspace/common/client/errors'; +import { auth } from '@workspace/common/client/firebase/config'; +import type { FormProps } from '@workspace/common/client/form/components'; +import { logger } from '@workspace/common/logger'; + +import type { LoginRequestData, LoginResponseData } from '~pages/api/auth/login'; +import type { VerifyLoginRequestData, VerifyLoginResponseData } from '~pages/api/webauthn/login/verify'; + +import { useExampleRouter } from '../../router'; +import type { LoginFormSchema, LoginFormValues } from '../schema'; + +export interface UseLoginWithEmailAndPasswordProps { + onSuccess: () => void; +} + +export function useLoginWithEmailAndPassword({ + onSuccess, +}: UseLoginWithEmailAndPasswordProps): FormProps['onSubmit'] { + const { redirect } = useExampleRouter(); + + return async function loginWithEmailAndPassword({ email, password }, { setError }) { + try { + const { data: loginResult } = await fetcher({ + method: 'POST', + url: '/auth/login', + body: { email, password } satisfies LoginRequestData, + }); + + if (!loginResult.mfa) { + await signInWithCustomToken(auth(), loginResult.customToken); + + redirect('/passkeys'); + + return; + } + + logger.info('/auth/login', loginResult); + + const webAuthnResult = await startAuthentication({ + optionsJSON: loginResult.publicKeyOptions, + }); + + logger.info('WebAuthn API result:', webAuthnResult); + + const { data: webAuthnVerifiedResult } = await fetcher({ + method: 'POST', + url: '/webauthn/login/verify', + body: { + authenticationResponse: webAuthnResult, + } satisfies VerifyLoginRequestData, + }); + + logger.info('/webauthn/login/verify', webAuthnVerifiedResult); + + await signInWithCustomToken(auth(), webAuthnVerifiedResult.customToken); + + onSuccess(); + + redirect('/passkeys'); + } catch (error) { + const parsedError = await parseUnknownError(error); + + setError('root', { + message: parsedError.message, + }); + + logger.error(error); + } + }; +} diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithEmailAndPasswordPage/index.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithEmailAndPasswordPage/index.ts new file mode 100644 index 0000000..e2c1bf8 --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithEmailAndPasswordPage/index.ts @@ -0,0 +1 @@ +export * from './LoginWithEmailAndPasswordPage'; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithEmailAndPasswordPage/schema/index.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithEmailAndPasswordPage/schema/index.ts new file mode 100644 index 0000000..adc00f7 --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithEmailAndPasswordPage/schema/index.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +import { email, password } from '@workspace/common/client/form/validators'; + +export const loginFormSchema = z.object({ + email, + password, +}); + +export type LoginFormSchema = typeof loginFormSchema; + +export type LoginFormValues = z.infer; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithPasskeyPage/LoginWithPasskeyPage.tsx b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithPasskeyPage/LoginWithPasskeyPage.tsx new file mode 100644 index 0000000..76d105f --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithPasskeyPage/LoginWithPasskeyPage.tsx @@ -0,0 +1,80 @@ +import { Alert, Box, Button, Divider, Stack, Words } from '@workspace/common/client/ui-kit'; +import { Email, Fingerprint, Google } from '@workspace/common/client/ui-kit/icons'; + +import { AuthFormContainer } from '../AuthFormContainer'; +import { useExampleRouter } from '../router'; +import { useLoginWithPasskey } from './hooks/useLoginWithPasskey'; + +export const LoginWithPasskeyPage = () => { + const loginWithPasskey = useLoginWithPasskey(); + const { redirect } = useExampleRouter(); + + return ( + <> + + + {loginWithPasskey.error && {loginWithPasskey.error.message}} + + + + + + ({ + textAlign: 'center', + color: theme.palette.text.secondary, + })} + > + Or continue with + + + + + + + + + + Don't have any account yet? + + + + + + ); +}; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithPasskeyPage/hooks/useLoginWithPasskey.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithPasskeyPage/hooks/useLoginWithPasskey.ts new file mode 100644 index 0000000..7caa101 --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithPasskeyPage/hooks/useLoginWithPasskey.ts @@ -0,0 +1,55 @@ +import { startAuthentication } from '@simplewebauthn/browser'; +import { useMutation } from '@tanstack/react-query'; +import { signInWithCustomToken } from 'firebase/auth'; + +import { fetcher } from '@workspace/common/client/api/fetcher'; +import { parseUnknownError } from '@workspace/common/client/errors'; +import { auth } from '@workspace/common/client/firebase/config'; +import { logger } from '@workspace/common/logger'; + +import type { StartLoginResponseData } from '~pages/api/webauthn/login/options'; +import type { VerifyLoginRequestData, VerifyLoginResponseData } from '~pages/api/webauthn/login/verify'; + +import { useExampleRouter } from '../../router'; + +export function useLoginWithPasskey() { + const { redirect } = useExampleRouter(); + + return useMutation>>({ + mutationFn: async () => { + try { + const { + data: { publicKeyOptions }, + } = await fetcher({ + method: 'GET', + url: '/webauthn/login/options', + }); + + logger.info('/webauthn/login/options', { publicKeyOptions }); + + const result = await startAuthentication({ + optionsJSON: publicKeyOptions, + }); + + logger.info('WebAuthn API result:', result); + + const { data } = await fetcher({ + method: 'POST', + url: '/webauthn/login/verify', + body: { + authenticationResponse: result, + } satisfies VerifyLoginRequestData, + }); + + logger.info('/webauthn/login/verify', { data }); + + await signInWithCustomToken(auth(), data.customToken); + + // NOTE: The Authorization header with ID token is set in request inceptor in AuthProvider.tsx component. + redirect('/passkeys'); + } catch (error) { + throw await parseUnknownError(error); + } + }, + }); +} diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithPasskeyPage/index.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithPasskeyPage/index.ts new file mode 100644 index 0000000..4f8ec35 --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/LoginWithPasskeyPage/index.ts @@ -0,0 +1 @@ +export * from './LoginWithPasskeyPage'; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/PasskeysPage/PasskeysPage.tsx b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/PasskeysPage/PasskeysPage.tsx new file mode 100644 index 0000000..e640fb5 --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/PasskeysPage/PasskeysPage.tsx @@ -0,0 +1,47 @@ +import { useDialog } from '@workspace/common/client/dialog/hooks'; +import { useAuthTokenClaims } from '@workspace/common/client/example/components'; +import { + PasskeysHeader, + PasskeysList, + PostRemovalDialog, + type PostRemovalDialogProps, +} from '@workspace/common/client/passkeys/components'; +import { Words } from '@workspace/common/client/ui-kit'; + +import { tokenClaims } from '~server/constans/tokenClaims'; + +import { useAddPasskey } from './hooks/useAddPasskey'; +import { useRemovePasskey } from './hooks/useRemovePasskey'; + +export const PasskeysPage = () => { + const addPasskey = useAddPasskey(); + const postRemovalDialog = useDialog(); + const removePasskey = useRemovePasskey(postRemovalDialog.openDialog); + const claims = useAuthTokenClaims(); + + return ( + <> + + {claims?.[tokenClaims.MFA_ENABLED] ? ( + true} /> + ) : ( + ({ + mt: 4, + textAlign: 'center', + color: theme.palette.text.secondary, + })} + > + Enable multi-factor authentication by adding a passkey. + + )} + + + ); +}; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/PasskeysPage/hooks/useAddPasskey.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/PasskeysPage/hooks/useAddPasskey.ts new file mode 100644 index 0000000..91fc056 --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/PasskeysPage/hooks/useAddPasskey.ts @@ -0,0 +1,62 @@ +import { startRegistration } from '@simplewebauthn/browser'; +import { useMutation } from '@tanstack/react-query'; + +import { queryClient } from '@workspace/common/client/api/components'; +import { fetcher } from '@workspace/common/client/api/fetcher'; +import { parseUnknownError } from '@workspace/common/client/errors'; +import { useSnack } from '@workspace/common/client/snackbar/hooks'; +import { logger } from '@workspace/common/logger'; + +import type { StartLinkingResponseData } from '~pages/api/webauthn/link/options'; +import type { VerifyLinkResponseData } from '~pages/api/webauthn/link/verify'; + +import { reauthenticate } from '../utils/reauthenticate'; + +export function useAddPasskey() { + const snack = useSnack(); + + return useMutation({ + mutationFn: async () => { + const { + data: { publicKeyOptions }, + } = await fetcher({ + method: 'GET', + url: '/webauthn/link/options', + }); + + logger.info('/webauthn/link/options', publicKeyOptions); + + const result = await startRegistration({ + optionsJSON: publicKeyOptions, + }); + + logger.info('Registration result:', result); + + const { + data: { customToken }, + } = await fetcher({ + method: 'POST', + url: '/webauthn/link/verify', + body: { + registrationResponse: result, + }, + }); + + await reauthenticate(customToken); + + await queryClient.invalidateQueries({ + queryKey: ['passkeys'], + }); + }, + async onError(error: Error) { + const parsedError = await parseUnknownError(error); + + logger.error(parsedError); + + snack('error', parsedError.message); + }, + onSuccess() { + snack('success', 'Passkey has been successfully added.'); + }, + }); +} diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/PasskeysPage/hooks/useRemovePasskey.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/PasskeysPage/hooks/useRemovePasskey.ts new file mode 100644 index 0000000..6b799a6 --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/PasskeysPage/hooks/useRemovePasskey.ts @@ -0,0 +1,77 @@ +import { startAuthentication } from '@simplewebauthn/browser'; +import { useMutation } from '@tanstack/react-query'; + +import { queryClient } from '@workspace/common/client/api/components'; +import { fetcher } from '@workspace/common/client/api/fetcher'; +import { parseUnknownError } from '@workspace/common/client/errors'; +import { useAuthUser } from '@workspace/common/client/example/components'; +import type { PostRemovalDialogProps } from '@workspace/common/client/passkeys/components'; +import { useSnack } from '@workspace/common/client/snackbar/hooks'; +import { logger } from '@workspace/common/logger'; + +import type { StartRemovalResponseData } from '~pages/api/webauthn/remove/options'; +import type { VerifyRemovalRequestData, VerifyRemovalResponseData } from '~pages/api/webauthn/remove/verify'; + +import { reauthenticate } from '../utils/reauthenticate'; + +/** + * Remove a passkey from the user's account. User must verify their identity before removing the passkey. + */ +export function useRemovePasskey(openDialog: (data: PostRemovalDialogProps['data']) => void) { + const snack = useSnack(); + const authUser = useAuthUser(); + + return useMutation({ + mutationFn: async (passkeyId: string) => { + try { + const { + data: { publicKeyOptions }, + } = await fetcher({ + method: 'GET', + url: '/webauthn/remove/options', + }); + + logger.info('/webauthn/remove/options', publicKeyOptions); + + const result = await startAuthentication({ + optionsJSON: publicKeyOptions, + }); + + logger.info('WebAuthn API result:', result); + + const { data: removalResult } = await fetcher({ + method: 'POST', + url: '/webauthn/remove/verify', + body: { + authenticationResponse: result, + passkeyId, + } satisfies VerifyRemovalRequestData, + }); + + const { mfa, passkey } = removalResult; + + if (!mfa) { + await reauthenticate(removalResult.customToken); + } + + if (passkey.provider) { + openDialog({ + provider: passkey.provider, + rpId: passkey.rpId, + username: authUser?.email!, + }); + } + + await queryClient.invalidateQueries({ + queryKey: ['passkeys'], + }); + } catch (error) { + const parsedError = await parseUnknownError(error); + + snack('error', parsedError.message); + + logger.error(error); + } + }, + }); +} diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/PasskeysPage/index.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/PasskeysPage/index.ts new file mode 100644 index 0000000..1d1082a --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/PasskeysPage/index.ts @@ -0,0 +1 @@ +export * from './PasskeysPage'; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/PasskeysPage/utils/reauthenticate.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/PasskeysPage/utils/reauthenticate.ts new file mode 100644 index 0000000..409812e --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/PasskeysPage/utils/reauthenticate.ts @@ -0,0 +1,8 @@ +import { signInWithCustomToken, signOut } from '@firebase/auth'; + +import { auth } from '@workspace/common/client/firebase/config'; + +export async function reauthenticate(customToken: string) { + await signOut(auth()); + await signInWithCustomToken(auth(), customToken); +} diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/RegisterWithEmailAndPasswordPage/RegisterWithEmailAndPasswordPage.tsx b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/RegisterWithEmailAndPasswordPage/RegisterWithEmailAndPasswordPage.tsx new file mode 100644 index 0000000..9eb423e --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/RegisterWithEmailAndPasswordPage/RegisterWithEmailAndPasswordPage.tsx @@ -0,0 +1,50 @@ +import { + EmailField, + FieldsStack, + Form, + FormError, + PasswordField, + SubmitButton, +} from '@workspace/common/client/form/components'; +import { Box, Button, Divider, Words } from '@workspace/common/client/ui-kit'; +import { Send } from '@workspace/common/client/ui-kit/icons'; + +import { AuthFormContainer } from '../AuthFormContainer'; +import { useExampleRouter } from '../router'; +import { useRegisterWithEmailAndPassword } from './hooks'; +import { registerFormSchema, type RegisterFormSchema, type RegisterFormValues } from './schema'; + +export const RegisterWithEmailAndPasswordPage = () => { + const register = useRegisterWithEmailAndPassword(); + const { redirect } = useExampleRouter(); + + return ( + + + schema={registerFormSchema} + onSubmit={register} + mode='onTouched' + > + + + name='email' autoComplete='email' /> + name='password' autoComplete='new-password' /> + + + }> + Register + + + + + + + Already have an account? + + + + + ); +}; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/RegisterWithEmailAndPasswordPage/hooks/index.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/RegisterWithEmailAndPasswordPage/hooks/index.ts new file mode 100644 index 0000000..fe4a58e --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/RegisterWithEmailAndPasswordPage/hooks/index.ts @@ -0,0 +1 @@ +export * from './useRegisterWithEmailAndPassword'; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/RegisterWithEmailAndPasswordPage/hooks/useRegisterWithEmailAndPassword.tsx b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/RegisterWithEmailAndPasswordPage/hooks/useRegisterWithEmailAndPassword.tsx new file mode 100644 index 0000000..1a8364b --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/RegisterWithEmailAndPasswordPage/hooks/useRegisterWithEmailAndPassword.tsx @@ -0,0 +1,74 @@ +import { sendEmailVerification, signInWithCustomToken, signOut, type User } from 'firebase/auth'; + +import { fetcher } from '@workspace/common/client/api/fetcher'; +import { parseUnknownError } from '@workspace/common/client/errors'; +import { auth } from '@workspace/common/client/firebase/config'; +import type { FormProps } from '@workspace/common/client/form/components'; +import { useSnack } from '@workspace/common/client/snackbar/hooks'; +import { logger } from '@workspace/common/logger'; + +import type { RegisterRequestData, RegisterResponseData } from '~pages/api/auth/register'; + +import { useExampleRouter } from '../../router'; +import type { RegisterFormSchema, RegisterFormValues } from '../schema'; + +async function sendUserEmailVerification(user: User) { + const returnUrl = new URL('/', window.location.origin); + + returnUrl.searchParams.set('verified', 'true'); + returnUrl.searchParams.set('email', user.email!); + + await sendEmailVerification(user, { + handleCodeInApp: true, + url: returnUrl.toString(), + }); +} + +export function useRegisterWithEmailAndPassword(): FormProps['onSubmit'] { + const { redirect } = useExampleRouter(); + const snack = useSnack(); + + return async function registerPasskey({ email, password }, { setError, reset }) { + try { + const { + data: { customToken }, + } = await fetcher({ + method: 'POST', + url: '/auth/register', + body: { email, password } satisfies RegisterRequestData, + }); + + const { user } = await signInWithCustomToken(auth(), customToken); + + if (!user.emailVerified) { + await sendUserEmailVerification(user); + await signOut(auth()); + } else { + redirect('/passkeys'); + } + + reset(); + + snack( + 'success', + <> + User created. + {!user.emailVerified && ( + <> +
+ Please check your inbox and click on the email verification link. + + )} + , + ); + } catch (error) { + const parsedError = await parseUnknownError(error); + + setError('root', { + message: parsedError.message, + }); + + logger.error(error); + } + }; +} diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/RegisterWithEmailAndPasswordPage/index.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/RegisterWithEmailAndPasswordPage/index.ts new file mode 100644 index 0000000..8b34237 --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/RegisterWithEmailAndPasswordPage/index.ts @@ -0,0 +1 @@ +export * from './RegisterWithEmailAndPasswordPage'; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/RegisterWithEmailAndPasswordPage/schema/index.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/RegisterWithEmailAndPasswordPage/schema/index.ts new file mode 100644 index 0000000..4253596 --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/RegisterWithEmailAndPasswordPage/schema/index.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +import { email, newPassword } from '@workspace/common/client/form/validators'; + +export const registerFormSchema = z.object({ + email, + password: newPassword, +}); + +export type RegisterFormSchema = typeof registerFormSchema; + +export type RegisterFormValues = z.infer; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/ResolveInitRoute/ResolveInitRoute.tsx b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/ResolveInitRoute/ResolveInitRoute.tsx new file mode 100644 index 0000000..eed1082 --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/ResolveInitRoute/ResolveInitRoute.tsx @@ -0,0 +1,27 @@ +import { useEffect } from 'react'; + +import { useExampleAuthSession } from '@workspace/common/client/example/components'; + +import { useExampleRouter } from '../router'; + +export const ResolveInitRoute = () => { + const { currentRoute, redirect } = useExampleRouter(); + const { session } = useExampleAuthSession(); + + useEffect(() => { + if (currentRoute !== null) { + return; + } + + switch (session.state) { + case 'authenticated': + redirect('/passkeys'); + break; + case 'unauthenticated': + redirect('/register'); + break; + } + }, [session, currentRoute, redirect]); + + return null; +}; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/ResolveInitRoute/index.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/ResolveInitRoute/index.ts new file mode 100644 index 0000000..06c3fba --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/ResolveInitRoute/index.ts @@ -0,0 +1 @@ +export * from './ResolveInitRoute'; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/UpgradeExample.tsx b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/UpgradeExample.tsx new file mode 100644 index 0000000..117a94d --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/UpgradeExample.tsx @@ -0,0 +1,33 @@ +import { useState } from 'react'; + +import { ExampleAuth, ExampleBody, ExampleFrame } from '@workspace/common/client/example/components'; + +import { DefaultExampleTopBar } from './DefaultExampleTopBar'; +import { EmailVerificationCode } from './EmailVerificationCode'; +import { EmailVerifiedAlert } from './EmailVerifiedAlert'; +import { ResolveInitRoute } from './ResolveInitRoute'; +import { CurrentExampleRoute, ExampleRouter } from './router'; +import { exampleRoutes } from './routes'; + +export const UpgradeExample = () => { + const [expanded, setExpanded] = useState(true); + + return ( + + + + + + + + + + + + + + + + + ); +}; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/index.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/index.ts new file mode 100644 index 0000000..f9a667d --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/index.ts @@ -0,0 +1 @@ +export * from './UpgradeExample'; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/router/index.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/router/index.ts new file mode 100644 index 0000000..58d5b70 --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/router/index.ts @@ -0,0 +1,7 @@ +import { createExampleRouter } from '@workspace/common/client/example/components'; + +import type { ExampleRoutes } from '../routes'; + +const { ExampleRouter, useExampleRouter, CurrentExampleRoute } = createExampleRouter(); + +export { CurrentExampleRoute, ExampleRouter, useExampleRouter }; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/routes/index.tsx b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/routes/index.tsx new file mode 100644 index 0000000..cc833fe --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/UpgradeExample/routes/index.tsx @@ -0,0 +1,16 @@ +import type { UnknownRoutes } from '@workspace/common/client/example/components'; + +import { LoginWithEmailAndPasswordPage } from '../LoginWithEmailAndPasswordPage'; +import { LoginWithPasskeyPage } from '../LoginWithPasskeyPage'; +import { PasskeysPage } from '../PasskeysPage'; +import { RegisterWithEmailAndPasswordPage } from '../RegisterWithEmailAndPasswordPage'; + +export const exampleRoutes = { + '/register': RegisterWithEmailAndPasswordPage, + '/login': LoginWithPasskeyPage, + '/login-with-password': LoginWithEmailAndPasswordPage, + '/passkeys': PasskeysPage, +} as const satisfies UnknownRoutes; + +export type ExampleRoutes = typeof exampleRoutes; +export type ExampleRoute = keyof ExampleRoutes; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/WebAuthnUpgradeExamplePage.tsx b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/WebAuthnUpgradeExamplePage.tsx new file mode 100644 index 0000000..6d3e295 --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/WebAuthnUpgradeExamplePage.tsx @@ -0,0 +1,42 @@ +import Link from 'next/link'; + +import { ExampleDescription, ExampleHeader, ExampleWrapper } from '@workspace/common/client/example/components'; +import { PageHeader } from '@workspace/common/client/layout/components'; +import { Container } from '@workspace/common/client/ui-kit'; + +import { UpgradeExample } from './UpgradeExample'; + +export const WebAuthnUpgradeExamplePage = () => { + return ( + <> + + + + + + Built with{' '} + + SimpleWebAuthn + {' '} + , Firebase Auth and Firestore SDKs. + , + ]} + /> + } + githubUrl='https://github.com/cermakjiri/with-webauthn/tree/dev/examples/webauthn-upgrade' + /> + + + + + ); +}; diff --git a/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/index.ts b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/index.ts new file mode 100644 index 0000000..313d828 --- /dev/null +++ b/examples/webauthn-upgrade/src/components/WebAuthnUpgradeExamplePage/index.ts @@ -0,0 +1 @@ +export * from './WebAuthnUpgradeExamplePage'; diff --git a/examples/webauthn-upgrade/src/instrumentation.ts b/examples/webauthn-upgrade/src/instrumentation.ts new file mode 100644 index 0000000..83253dc --- /dev/null +++ b/examples/webauthn-upgrade/src/instrumentation.ts @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/nextjs'; + +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + await import('../sentry.server.config'); + } + + if (process.env.NEXT_RUNTIME === 'edge') { + await import('../sentry.edge.config'); + } +} + +export const onRequestError = Sentry.captureRequestError; diff --git a/examples/webauthn-upgrade/src/pages/_app.tsx b/examples/webauthn-upgrade/src/pages/_app.tsx new file mode 100755 index 0000000..f7ebc4e --- /dev/null +++ b/examples/webauthn-upgrade/src/pages/_app.tsx @@ -0,0 +1,3 @@ +import { App } from '@workspace/common/client/core/components'; + +export default App; diff --git a/examples/webauthn-upgrade/src/pages/_document.tsx b/examples/webauthn-upgrade/src/pages/_document.tsx new file mode 100644 index 0000000..30166d9 --- /dev/null +++ b/examples/webauthn-upgrade/src/pages/_document.tsx @@ -0,0 +1,3 @@ +import { MyDocument } from '@workspace/common/client/core/components'; + +export default MyDocument; diff --git a/examples/webauthn-upgrade/src/pages/_error.jsx b/examples/webauthn-upgrade/src/pages/_error.jsx new file mode 100644 index 0000000..df405cb --- /dev/null +++ b/examples/webauthn-upgrade/src/pages/_error.jsx @@ -0,0 +1,3 @@ +import { CustomError } from '@workspace/common/client/core/components'; + +export default CustomError; diff --git a/examples/webauthn-upgrade/src/pages/api/auth/login.ts b/examples/webauthn-upgrade/src/pages/api/auth/login.ts new file mode 100644 index 0000000..6292a76 --- /dev/null +++ b/examples/webauthn-upgrade/src/pages/api/auth/login.ts @@ -0,0 +1,124 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { generateAuthenticationOptions } from '@simplewebauthn/server'; +import { generateChallenge } from '@simplewebauthn/server/helpers'; +import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types'; +import { signInWithEmailAndPassword } from 'firebase/auth'; +import z from 'zod'; + +import { env } from '@workspace/common/client/env'; +import { auth as clientAuth } from '@workspace/common/client/firebase/config'; +import { email, password } from '@workspace/common/client/form/validators'; +import { logger } from '@workspace/common/logger'; +import { auth as serverAuth } from '@workspace/common/server/config/firebase'; +import { initializeChallengeSession } from '@workspace/common/server/services/challenge-session'; +import { getPasskeys } from '@workspace/common/server/services/passkeys'; +import { getUserPasskeys } from '@workspace/common/server/services/users'; +import { getRpId } from '@workspace/common/server/utils'; + +import { tokenClaims } from '~server/constans/tokenClaims'; + +async function generateUserAuthenticationOptions(email: string, challenge: Uint8Array) { + const passkeys = await getUserPasskeys(email); + + const authenticationOptions = await generateAuthenticationOptions({ + /** + * Relying party ID: Hostname of your client app. + * E.g. "localhost", "example.com" or "auth.example.com". + */ + rpID: getRpId(env.NEXT_PUBLIC_CLIENT_ORIGIN), + + challenge, + + /** + * We might not want to provide list of available passkeys to unauthenticated users for privacy reasons: + * - https://w3c.github.io/webauthn/#sctn-credential-id-privacy-leak + * - https://w3c.github.io/webauthn/#sctn-unprotected-account-detection (This is not relevent for this demo but, I believe, developers should be aware of this when implementing WebAuthn in production.) + */ + allowCredentials: passkeys.map(({ credentialId, transports }) => ({ + id: credentialId, + transports, + })), + + /** + * User presense is not enough. Require user verification (e.g. PIN, fingerprint, face recognition) to verify user identity. + * If the authenticator does not support user verification, the registration will fail. + */ + userVerification: 'required', + + timeout: 300_000, + }); + + return authenticationOptions; +} + +export const loginRequestBody = z.object({ + email, + password, +}); + +export type LoginRequestData = z.infer; + +export type LoginResponseData = + | { + customToken: string; + mfa: false; + } + | { + publicKeyOptions: PublicKeyCredentialRequestOptionsJSON; + mfa: true; + }; + +/** + * 1. Verifies email/password combination. + * 2. Retrieves passkeys for given user. + * 3. If the user has some passkeys, then generate options for for WebAuthn sign-in (i.e. for the `navigator.credentials.get` method). + * 4. Else send custom token with `mfa_enabled: false` encoded flag. + */ +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { email, password } = loginRequestBody.parse(req.body); + + const userCrendential = await signInWithEmailAndPassword(clientAuth(), email, password); + + if (!userCrendential.user.emailVerified) { + res.status(400).end('Email not verified.'); + + return; + } + + const passkeys = await getPasskeys(userCrendential.user.uid); + + // User added has some passkeys (i.e. his account upgraded to MFA) + // The user must finish the authentication with passkeys. + if (passkeys.length > 0) { + // Generate a random string with enough entropy to prevent replay attacks. + const challenge = await generateChallenge(); + + const publicKeyOptions = await generateUserAuthenticationOptions(email, challenge); + + await initializeChallengeSession(res, { + type: 'assertion', + timeout: publicKeyOptions.timeout!, + challenge, + }); + + res.send({ publicKeyOptions, mfa: true }); + + return; + } + + const customToken = await serverAuth().createCustomToken(userCrendential.user.uid, { + [tokenClaims.MFA_ENABLED]: false, + }); + + res.send({ customToken, mfa: false }); + } catch (error) { + logger.error(error); + + res.status(500).end( + error instanceof Error && env.NEXT_PUBLIC_NODE_ENV !== 'production' + ? error.message + : 'Internal Server Error', + ); + } +} diff --git a/examples/webauthn-upgrade/src/pages/api/auth/register.ts b/examples/webauthn-upgrade/src/pages/api/auth/register.ts new file mode 100644 index 0000000..f1c8d0e --- /dev/null +++ b/examples/webauthn-upgrade/src/pages/api/auth/register.ts @@ -0,0 +1,70 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { AuthErrorCodes } from 'firebase/auth'; +import z from 'zod'; + +import { env } from '@workspace/common/client/env'; +import { email, password } from '@workspace/common/client/form/validators'; +import { logger } from '@workspace/common/logger'; +import { auth } from '@workspace/common/server/config/firebase'; + +import { tokenClaims } from '~server/constans/tokenClaims'; + +export const registerRequestBody = z.object({ + email, + password, +}); + +export type RegisterRequestData = z.infer; + +export type RegisterResponseData = { + customToken: string; +}; + +async function findUserByEmail(email: string) { + try { + return await auth().getUserByEmail(email); + } catch (error) { + // @ts-expect-error Yes, it be could parsed in a better way. + if (error.errorInfo?.code === AuthErrorCodes.USER_DELETED) { + return null; + } + + logger.error(error); + + return null; + } +} + +/** + * Classical email/password login if the user has not yet linked a passkey. + * Then, the it signs in the user with email/password and sends WebAuthn options for retrieving a passkey. + */ +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { email, password } = registerRequestBody.parse(req.body); + + const existingUser = await findUserByEmail(email); + + if (existingUser) { + return res.status(403).end('User with this email already exists.'); + } + + const user = await auth().createUser({ + email, + password, + emailVerified: false, + }); + + const customToken = await auth().createCustomToken(user.uid, { [tokenClaims.MFA_ENABLED]: false }); + + res.send({ customToken }); + } catch (error) { + logger.error(error); + + res.status(500).end( + error instanceof Error && env.NEXT_PUBLIC_NODE_ENV !== 'production' + ? error.message + : 'Internal Server Error', + ); + } +} diff --git a/examples/webauthn-upgrade/src/pages/api/webauthn/link/options.ts b/examples/webauthn-upgrade/src/pages/api/webauthn/link/options.ts new file mode 100644 index 0000000..41b2d92 --- /dev/null +++ b/examples/webauthn-upgrade/src/pages/api/webauthn/link/options.ts @@ -0,0 +1,125 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { generateRegistrationOptions } from '@simplewebauthn/server'; +import { generateChallenge } from '@simplewebauthn/server/helpers'; +import type { PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/types'; + +import { env } from '@workspace/common/client/env'; +import { logger } from '@workspace/common/logger'; +import { auth } from '@workspace/common/server/config/firebase'; +import { RP_NAME } from '@workspace/common/server/constants/relyingParty'; +import { initializeChallengeSession } from '@workspace/common/server/services/challenge-session'; +import { getPasskeys } from '@workspace/common/server/services/passkeys'; +import { createUserWithNoPasskeys, getUser } from '@workspace/common/server/services/users'; +import { getRpId, parseAndVerifyIdToken } from '@workspace/common/server/utils'; + +import { tokenClaims } from '~server/constans/tokenClaims'; + +export type StartLinkingResponseData = { + publicKeyOptions: PublicKeyCredentialCreationOptionsJSON; +}; + +/** + * Upgrade from email/password auth. provider to MFA with passkeys. + * - Check if user provided valid token. + * - Create user with no passkeys if user does not exist. + * - Proceed with WebAuthn registration. + */ +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const idTokenResult = await parseAndVerifyIdToken(req.headers.authorization); + + if (!idTokenResult || !idTokenResult.email_verified) { + logger.error('User not authenticated. No ID token or email not verified.'); + + return res.status(401).end('User not authenticated.'); + } + + const userId = idTokenResult.uid; + const authUser = await auth().getUser(userId); + const username = authUser.email!; + + const user = await getUser(userId); + + if (user === null) { + await createUserWithNoPasskeys(userId, username); + } + + const passkeys = await getPasskeys(userId); + + // ID token claims must include 'mfa_enabled: true' once at least one passkey has been added. + if (passkeys.length > 0 && !idTokenResult[tokenClaims.MFA_ENABLED]) { + logger.error('User has passkeys but MFA is not enabled.'); + + return res.status(401).end('User not authenticated.'); + } + + /** + * Generate a random string with enough entropy to be signed by the authenticator to prevent replay attacks. + */ + const challenge = await generateChallenge(); + + const registrationOptions = await generateRegistrationOptions({ + /** + * Relying party ID: Hostname of your client app. + * E.g. "localhost", "example.com" or "auth.example.com". + */ + rpID: getRpId(env.NEXT_PUBLIC_CLIENT_ORIGIN), + + /** + * Relying party name: Display name of your client app. + */ + rpName: RP_NAME, + + challenge, + + userName: username, + + /** + * Prevent creating multiple user passkeys on the same authenticator. + */ + excludeCredentials: passkeys.map(({ credentialId, transports }) => ({ + id: credentialId, + transports, + })), + + /** + * Require authenticator to provide proof of its origin. + */ + attestationType: 'direct', + + authenticatorSelection: { + /** + * User presense is not enough. Require user verification (e.g. PIN, fingerprint) to verify user identity. + * If the authenticator does not support user verification, the registration will fail. + */ + userVerification: 'required', + }, + + /** + * Recommended range: 300_000 milliseconds to 600_000 milliseconds. + * Recommended default value: 300_000 milliseconds (5 minutes). + * https://www.w3.org/TR/webauthn-3/#sctn-createCredential + */ + timeout: 300_000, + }); + + await initializeChallengeSession(res, { + type: 'attestation', + timeout: registrationOptions.timeout!, + challenge, + username, + }); + + res.status(200).json({ + publicKeyOptions: registrationOptions, + }); + } catch (error) { + logger.error(error); + + res.status(500).end( + error instanceof Error && env.NEXT_PUBLIC_NODE_ENV !== 'production' + ? error.message + : 'Internal Server Error', + ); + } +} diff --git a/examples/webauthn-upgrade/src/pages/api/webauthn/link/verify.ts b/examples/webauthn-upgrade/src/pages/api/webauthn/link/verify.ts new file mode 100644 index 0000000..67930fc --- /dev/null +++ b/examples/webauthn-upgrade/src/pages/api/webauthn/link/verify.ts @@ -0,0 +1,67 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { verifyRegistrationResponse } from '@simplewebauthn/server'; +import type { RegistrationResponseJSON } from '@simplewebauthn/types'; + +import { env } from '@workspace/common/client/env'; +import { logger } from '@workspace/common/logger'; +import { retrieveAndInvalidateChallengeSession } from '@workspace/common/server/services/challenge-session'; +import { addUserPasskey } from '@workspace/common/server/services/users'; + +import { tokenClaims } from '~server/constans/tokenClaims'; +import { revokenAndCreateCustomUserToken } from '~server/services/auth'; +import { parseAndVerifyIdTokenForMFA } from '~server/utils/parseAndVerifyIdTokenForMFA'; + +export type VerifyLinkRequestData = { + registrationResponse: RegistrationResponseJSON; +}; + +export type VerifyLinkResponseData = { + customToken: string; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const idTokenResult = await parseAndVerifyIdTokenForMFA(req.headers.authorization); + + if (!idTokenResult) { + return res.status(401).end('User not authenticated.'); + } + + const { registrationResponse } = req.body as VerifyLinkRequestData; + + const challengeSession = await retrieveAndInvalidateChallengeSession(req, res, 'attestation'); + + if (!challengeSession || challengeSession.type !== 'attestation') { + return res.status(401).end('Challenge session is not active. Please start the registration process again.'); + } + + const verifiedRegistrationResponse = await verifyRegistrationResponse({ + response: registrationResponse, + expectedChallenge: challengeSession.challenge, + expectedOrigin: challengeSession.origin, + expectedRPID: challengeSession.rpId, + }); + + if (!verifiedRegistrationResponse.verified) { + return res.status(401).end('User not authenticated.'); + } + + logger.debug('verifiedRegistrationResponse', verifiedRegistrationResponse); + + const userId = idTokenResult.uid; + + await addUserPasskey(userId, verifiedRegistrationResponse.registrationInfo); + + const customToken = await revokenAndCreateCustomUserToken(userId, { [tokenClaims.MFA_ENABLED]: true }); + + res.status(200).json({ customToken }); + } catch (error) { + logger.error(error); + + res.status(500).end( + error instanceof Error && env.NEXT_PUBLIC_NODE_ENV !== 'production' + ? error.message + : 'Internal Server Error', + ); + } +} diff --git a/examples/webauthn-upgrade/src/pages/api/webauthn/login/options.ts b/examples/webauthn-upgrade/src/pages/api/webauthn/login/options.ts new file mode 100644 index 0000000..3295fcf --- /dev/null +++ b/examples/webauthn-upgrade/src/pages/api/webauthn/login/options.ts @@ -0,0 +1,57 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { generateAuthenticationOptions } from '@simplewebauthn/server'; +import { generateChallenge } from '@simplewebauthn/server/helpers'; +import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types'; + +import { env } from '@workspace/common/client/env'; +import { logger } from '@workspace/common/logger'; +import { initializeChallengeSession } from '@workspace/common/server/services/challenge-session'; +import { getRpId } from '@workspace/common/server/utils'; + +export type StartLoginResponseData = { + publicKeyOptions: PublicKeyCredentialRequestOptionsJSON; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + // Generate a random string with enough entropy to prevent replay attacks. + const challenge = await generateChallenge(); + + const authenticationOptions = await generateAuthenticationOptions({ + /** + * Relying party ID: Hostname of your client app. + * E.g. "localhost", "example.com" or "auth.example.com". + */ + rpID: getRpId(env.NEXT_PUBLIC_CLIENT_ORIGIN), + + challenge, + + allowCredentials: [], + + /** + * User presense is not enough. Require user verification (e.g. PIN, fingerprint, face recognition) to verify user identity. + * If the authenticator does not support user verification, the registration will fail. + */ + userVerification: 'required', + + timeout: 300_000, + }); + + await initializeChallengeSession(res, { + type: 'assertion', + timeout: authenticationOptions.timeout!, + challenge, + }); + + res.status(200).json({ + publicKeyOptions: authenticationOptions, + }); + } catch (error) { + logger.error(error); + + res.status(500).end((error as Error).message); + // error instanceof Error && env.NEXT_PUBLIC_NODE_ENV !== 'production' + // ? error.message + // : 'Internal Server Error', + } +} diff --git a/examples/webauthn-upgrade/src/pages/api/webauthn/login/verify.ts b/examples/webauthn-upgrade/src/pages/api/webauthn/login/verify.ts new file mode 100644 index 0000000..9495b51 --- /dev/null +++ b/examples/webauthn-upgrade/src/pages/api/webauthn/login/verify.ts @@ -0,0 +1,87 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { base64URLStringToBuffer } from '@simplewebauthn/browser'; +import { verifyAuthenticationResponse } from '@simplewebauthn/server'; +import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; +import { FieldValue } from 'firebase-admin/firestore'; + +import { env } from '@workspace/common/client/env'; +import { logger } from '@workspace/common/logger'; +import { retrieveAndInvalidateChallengeSession } from '@workspace/common/server/services/challenge-session'; +import { getPasskeyBy, updatePasskey } from '@workspace/common/server/services/passkeys'; + +import { tokenClaims } from '~server/constans/tokenClaims'; +import { revokenAndCreateCustomUserToken } from '~server/services/auth'; + +export type VerifyLoginRequestData = { + authenticationResponse: AuthenticationResponseJSON; +}; + +export type VerifyLoginResponseData = { + customToken: string; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { authenticationResponse } = req.body as VerifyLoginRequestData; + + const challengeSession = await retrieveAndInvalidateChallengeSession(req, res, 'assertion'); + + if (!challengeSession || challengeSession.type !== 'assertion') { + return res.status(401).end('Challenge session is not active. Please start the login process again.'); + } + + const passkey = await getPasskeyBy('credentialId', authenticationResponse.id); + + // This might happen if the user has removed the passkey from the account + // but the private passkey source is still stored in a keychain / password manager. + if (!passkey) { + // Yes, this message would have been better however it could be a security risk (i.e. username enumeration): + // Checkout https://w3c.github.io/webauthn/#sctn-username-enumeration. + // return res.status(400).end('Passkey not found.'); + return res.status(401).end('User not verified.'); + } + + const { transports, credentialId, credentialPublicKey, credentialCounter } = passkey; + + const result = await verifyAuthenticationResponse({ + response: authenticationResponse, + expectedRPID: challengeSession.rpId, + expectedOrigin: challengeSession.origin, + expectedChallenge: challengeSession.challenge, + credential: { + publicKey: new Uint8Array(base64URLStringToBuffer(credentialPublicKey)), + counter: credentialCounter, + transports, + id: credentialId, + }, + requireUserVerification: true, + }); + + logger.debug('verifyAuthenticationResponse:', result); + + if (!result.verified) { + return res.status(401).end('User not verified.'); + } + + // Just an example what you can do with the result, not needed for this current authentication process itself + // parseAutheticationResponse(authenticationResponse) + + await updatePasskey(passkey.id, { + credentialCounter: result.authenticationInfo.newCounter, + // @ts-expect-error + lastUsedAt: FieldValue.serverTimestamp(), + }); + + const customToken = await revokenAndCreateCustomUserToken(passkey.userId, { [tokenClaims.MFA_ENABLED]: true }); + + res.status(200).json({ customToken }); + } catch (error) { + logger.error(error); + + res.status(500).end( + error instanceof Error && env.NEXT_PUBLIC_NODE_ENV !== 'production' + ? error.message + : 'Internal Server Error', + ); + } +} diff --git a/examples/webauthn-upgrade/src/pages/api/webauthn/remove/options.ts b/examples/webauthn-upgrade/src/pages/api/webauthn/remove/options.ts new file mode 100644 index 0000000..708b142 --- /dev/null +++ b/examples/webauthn-upgrade/src/pages/api/webauthn/remove/options.ts @@ -0,0 +1,73 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { generateAuthenticationOptions } from '@simplewebauthn/server'; +import { generateChallenge } from '@simplewebauthn/server/helpers'; +import type { PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/types'; + +import { env } from '@workspace/common/client/env'; +import { logger } from '@workspace/common/logger'; +import { initializeChallengeSession } from '@workspace/common/server/services/challenge-session'; +import { getPasskeys } from '@workspace/common/server/services/passkeys'; +import { getRpId } from '@workspace/common/server/utils'; + +import { parseAndVerifyIdTokenForMFA } from '~server/utils/parseAndVerifyIdTokenForMFA'; + +export type StartRemovalResponseData = { + publicKeyOptions: PublicKeyCredentialCreationOptionsJSON; +}; + +/** + * Request options for WebAuthn verification before requesting passkey removal. + */ +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const idTokenResult = await parseAndVerifyIdTokenForMFA(req.headers.authorization); + + if (!idTokenResult) { + return res.status(401).end('User not authenticated.'); + } + + const userId = idTokenResult.uid; + const passkeys = await getPasskeys(userId); + + // Generate a random string with enough entropy to prevent replay attacks. + const challenge = await generateChallenge(); + + const authenticationOptions = await generateAuthenticationOptions({ + /** + * Relying party ID: Hostname of your client app. + * E.g. "localhost", "example.com" or "auth.example.com". + */ + rpID: getRpId(env.NEXT_PUBLIC_CLIENT_ORIGIN), + + challenge, + + allowCredentials: passkeys.map(({ credentialId, transports }) => ({ id: credentialId, transports })), + + /** + * User presense is not enough. Require user verification (e.g. PIN, fingerprint, face recognition) to verify user identity. + * If the authenticator does not support user verification, the registration will fail. + */ + userVerification: 'required', + + timeout: 300_000, + }); + + await initializeChallengeSession(res, { + type: 'assertion', + timeout: authenticationOptions.timeout!, + challenge, + }); + + res.status(200).json({ + publicKeyOptions: authenticationOptions, + }); + } catch (error) { + logger.error(error); + + res.status(500).end( + error instanceof Error && env.NEXT_PUBLIC_NODE_ENV !== 'production' + ? error.message + : 'Internal Server Error', + ); + } +} diff --git a/examples/webauthn-upgrade/src/pages/api/webauthn/remove/verify.ts b/examples/webauthn-upgrade/src/pages/api/webauthn/remove/verify.ts new file mode 100644 index 0000000..fd843a1 --- /dev/null +++ b/examples/webauthn-upgrade/src/pages/api/webauthn/remove/verify.ts @@ -0,0 +1,125 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { base64URLStringToBuffer } from '@simplewebauthn/browser'; +import { verifyAuthenticationResponse } from '@simplewebauthn/server'; +import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; + +import { env } from '@workspace/common/client/env'; +import { logger } from '@workspace/common/logger'; +import { retrieveAndInvalidateChallengeSession } from '@workspace/common/server/services/challenge-session'; +import { getPasskey, getPasskeyBy, getPasskeys } from '@workspace/common/server/services/passkeys'; +import { removeUserPasskey } from '@workspace/common/server/services/users'; +import type { Passkey } from '@workspace/common/types'; + +import { tokenClaims } from '~server/constans/tokenClaims'; +import { revokenAndCreateCustomUserToken } from '~server/services/auth'; +import { parseAndVerifyIdTokenForMFA } from '~server/utils/parseAndVerifyIdTokenForMFA'; + +export type VerifyRemovalRequestData = { + authenticationResponse: AuthenticationResponseJSON; + passkeyId: string; +}; + +export type VerifyRemovalResponseData = + | { + /** + * Removed passkey. + */ + passkey: Passkey; + mfa: true; + } + | { + /** + * Removed passkey. + */ + passkey: Passkey; + + /** + * Once all passkeys have been removed, a new token is generated with `mfa_enabled: false` flag. + */ + customToken: string; + mfa: false; + }; + +/** + * Verify the user's identity before removing the passkey. + */ +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const idTokenResult = await parseAndVerifyIdTokenForMFA(req.headers.authorization); + + if (!idTokenResult) { + return res.status(401).end('User not authenticated.'); + } + + const challengeSession = await retrieveAndInvalidateChallengeSession(req, res, 'assertion'); + + if (!challengeSession || challengeSession.type !== 'assertion') { + return res.status(401).end('Challenge session is not active. Please start the login process again.'); + } + + const { authenticationResponse, passkeyId } = req.body as VerifyRemovalRequestData; + + const passkeyForAuthentication = await getPasskeyBy('credentialId', authenticationResponse.id); + + if (!passkeyForAuthentication) { + return res.status(400).end('Passkey for authenticaiton not found.'); + } + + const { transports, credentialId, credentialPublicKey, credentialCounter } = passkeyForAuthentication; + + const result = await verifyAuthenticationResponse({ + response: authenticationResponse, + expectedRPID: challengeSession.rpId, + expectedOrigin: challengeSession.origin, + expectedChallenge: challengeSession.challenge, + credential: { + publicKey: new Uint8Array(base64URLStringToBuffer(credentialPublicKey)), + counter: credentialCounter, + transports, + id: credentialId, + }, + requireUserVerification: true, + }); + + logger.debug('verifyAuthenticationResponse:', result); + + if (!result.verified) { + return res.status(401).end('User not verified.'); + } + + const passkeyForRemoval = await getPasskey(passkeyId); + + if (!passkeyForRemoval) { + return res.status(400).end('Passkey for removal not found.'); + } + + const userId = passkeyForRemoval.userId; + + await removeUserPasskey(userId, passkeyForRemoval.id); + + const userPasskeys = await getPasskeys(userId); + + if (userPasskeys.length === 0) { + const customToken = await revokenAndCreateCustomUserToken(userId, { [tokenClaims.MFA_ENABLED]: false }); + + res.status(200).json({ + mfa: false, + customToken, + passkey: passkeyForRemoval, + }); + } else { + res.status(200).json({ + mfa: true, + passkey: passkeyForRemoval, + }); + } + } catch (error) { + logger.error(error); + + res.status(500).end( + error instanceof Error && env.NEXT_PUBLIC_NODE_ENV !== 'production' + ? error.message + : 'Internal Server Error', + ); + } +} diff --git a/examples/webauthn-upgrade/src/pages/index.tsx b/examples/webauthn-upgrade/src/pages/index.tsx new file mode 100644 index 0000000..efcfa1c --- /dev/null +++ b/examples/webauthn-upgrade/src/pages/index.tsx @@ -0,0 +1,3 @@ +import { WebAuthnUpgradeExamplePage } from '~components/WebAuthnUpgradeExamplePage'; + +export default WebAuthnUpgradeExamplePage; diff --git a/examples/webauthn-upgrade/src/server/constans/tokenClaims.ts b/examples/webauthn-upgrade/src/server/constans/tokenClaims.ts new file mode 100644 index 0000000..0de96e5 --- /dev/null +++ b/examples/webauthn-upgrade/src/server/constans/tokenClaims.ts @@ -0,0 +1,3 @@ +export const tokenClaims = { + MFA_ENABLED: 'mfa_enabled', +} as const; diff --git a/examples/webauthn-upgrade/src/server/services/auth/index.ts b/examples/webauthn-upgrade/src/server/services/auth/index.ts new file mode 100644 index 0000000..a2d49fb --- /dev/null +++ b/examples/webauthn-upgrade/src/server/services/auth/index.ts @@ -0,0 +1,9 @@ +import { auth } from '@workspace/common/server/config/firebase'; + +export async function revokenAndCreateCustomUserToken(userId: string, claims: TokenClaims) { + await auth().revokeRefreshTokens(userId); + + const customToken = await auth().createCustomToken(userId, claims); + + return customToken; +} diff --git a/examples/webauthn-upgrade/src/server/utils/parseAndVerifyIdTokenForMFA.ts b/examples/webauthn-upgrade/src/server/utils/parseAndVerifyIdTokenForMFA.ts new file mode 100644 index 0000000..447a5e5 --- /dev/null +++ b/examples/webauthn-upgrade/src/server/utils/parseAndVerifyIdTokenForMFA.ts @@ -0,0 +1,28 @@ +import type { DecodedIdToken } from 'firebase-admin/auth'; + +import { getPasskeys } from '@workspace/common/server/services/passkeys'; +import { parseAndVerifyIdToken } from '@workspace/common/server/utils'; + +import { tokenClaims } from '~server/constans/tokenClaims'; + +export type DecodedIdTokenForMFA = DecodedIdToken & { + mfa_enabled: boolean; +}; + +export async function parseAndVerifyIdTokenForMFA(authorizationHeader: string | undefined) { + const decodedIdToken = await parseAndVerifyIdToken(authorizationHeader); + + if (!decodedIdToken || !decodedIdToken.email_verified) { + return null; + } + + const userPasskeys = await getPasskeys(decodedIdToken.uid); + + const mfaEnabled = decodedIdToken[tokenClaims.MFA_ENABLED]; + + if ((mfaEnabled && userPasskeys.length > 0) || (!mfaEnabled && userPasskeys.length === 0)) { + return decodedIdToken as DecodedIdTokenForMFA; + } + + return null; +} diff --git a/examples/webauthn-upgrade/tsconfig.json b/examples/webauthn-upgrade/tsconfig.json new file mode 100755 index 0000000..c944d8b --- /dev/null +++ b/examples/webauthn-upgrade/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "@tooling/typescript/nextjs", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "~*": [ + "./src/*", + ], + "~public/*": [ + "./public/*" + ], + "@workspace/common/*": [ + "../../packages/common/dist/*" + ] + } + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 0562cad..23b2293 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,22 @@ { "name": "with-webauthn", "description": "A project with full-stack WebAuthn API examples.", - "packageManager": "yarn@4.5.0", + "packageManager": "yarn@4.5.3+sha512.3003a14012e2987072d244c720506549c1aab73ee728208f1b2580a9fd67b92d61ba6b08fe93f6dce68fd771e3af1e59a0afa28dd242dd0940d73b95fedd4e90", "private": true, - "type": "commonjs", + "type": "module", "engines": { - "node": "20" + "node": "22" }, "scripts": { "postinstall": "turbo telemetry disable", - "build": "turbo run build", - "dev": "turbo run dev --concurrency 100%", + "build": "yarn workspace @workspace/common build && turbo run build --parallel", + "dev": "yarn workspace @workspace/common build && turbo run dev --concurrency 100%", "cir-dep": "turbo run cir-dep --parallel", "test:ci": "turbo run test:ci", "lint": "turbo run lint --parallel", "lint:fix": "turbo run lint --parallel -- --fix", "format": "turbo run format --parallel", - "audit": "yarn npm audit --severity moderate --environment production --all", + "audit": "yarn npm audit --severity moderate --all", "pre-commit": "prettier-format --staged --log-level=log && turbo run lint --parallel --force -- --staged --fix", "prepare": "husky" }, @@ -30,8 +30,8 @@ "@tooling/prettier": "workspace:*", "@tooling/typescript": "workspace:*", "dotenv": "16.4.5", - "husky": "9.1.6", - "turbo": "2.2.3" + "husky": "9.1.7", + "turbo": "2.3.3" }, "prettier": "@tooling/prettier/config", "license": "GPL-3.0-only", diff --git a/packages/common/package.json b/packages/common/package.json index 788156e..224b8c0 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -8,38 +8,42 @@ "lint:fix": "yarn lint --fix", "format": "prettier-format", "cir-dep": "check-cir-deps .", - "build": "tsc && tsc-alias" + "build": "tsc && tsc-alias", + "clean": "rm -rf dist .cache", + "dev": "tsc -w & tsc-alias -w" }, "dependencies": { "@ackee/antonio-core": "5.0.0", - "@emotion/react": "11.13.3", + "@emotion/react": "11.13.5", "@emotion/server": "11.11.0", - "@emotion/styled": "11.13.0", + "@emotion/styled": "11.13.5", "@hookform/resolvers": "3.9.1", - "@mui/icons-material": "6.1.6", + "@mui/icons-material": "6.1.8", "@mui/lab": "6.0.0-beta.14", - "@mui/material": "6.1.6", - "@sentry/nextjs": "8.37.1", + "@mui/material": "6.1.8", + "@mui/system": "6.1.8", + "@sentry/nextjs": "8.41.0", "@simplewebauthn/browser": "11.0.0", "@simplewebauthn/server": "11.0.0", "@t3-oss/env-nextjs": "0.11.1", - "@tanstack/react-query": "5.59.20", - "@tanstack/react-query-devtools": "5.59.20", + "@tanstack/react-query": "5.61.5", + "@tanstack/react-query-devtools": "5.61.5", "@workspace/logger": "workspace:*", - "cookie": "1.0.1", + "cookie": "1.0.2", "core-js": "3.39.0", - "firebase": "11.0.1", - "firebase-admin": "12.7.0", + "firebase": "11.0.2", + "firebase-admin": "13.0.1", "next": "15.0.3", "normalize.css": "8.0.1", + "nuqs": "2.2.3", "radash": "12.1.0", "react": "18.3.1", "react-dom": "18.3.1", - "react-hook-form": "7.53.1", - "react-intl": "6.8.7", + "react-hook-form": "7.53.2", + "react-intl": "7.0.1", "react-toastify": "10.0.6", "reset.css": "2.0.2", - "typescript": "5.6.3", + "typescript": "5.7.2", "zod": "3.23.8" }, "devDependencies": { diff --git a/packages/common/src/client/api/components/QueryEmpty/QueryEmpty.styles.ts b/packages/common/src/client/api/components/QueryEmpty/QueryEmpty.styles.ts new file mode 100644 index 0000000..00a4e3b --- /dev/null +++ b/packages/common/src/client/api/components/QueryEmpty/QueryEmpty.styles.ts @@ -0,0 +1,9 @@ +import { styled } from '@mui/material'; + +export const Empty = styled('section')(({ theme }) => ({ + padding: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + gap: theme.spacing(3), +})); diff --git a/packages/common/src/client/api/components/QueryEmpty/QueryEmpty.tsx b/packages/common/src/client/api/components/QueryEmpty/QueryEmpty.tsx new file mode 100644 index 0000000..7fb3f0e --- /dev/null +++ b/packages/common/src/client/api/components/QueryEmpty/QueryEmpty.tsx @@ -0,0 +1,71 @@ +import type { ReactNode } from 'react'; +import type { RequestResult } from '@ackee/antonio-core'; +import { Refresh } from '@mui/icons-material'; +import type { UseQueryResult } from '@tanstack/react-query'; + +import { Button, Icon, Words, type AlertProps } from '~client/ui-kit'; + +import { isAnyResultLoading } from '../QueryLoader'; +import { Empty } from './QueryEmpty.styles'; + +type Result = UseQueryResult>; + +export interface QueryEmptyProps extends Omit { + children: React.ReactNode; + result: T; + message: ReactNode; + isEmpty?: (result: T) => boolean; + retry?: boolean; +} + +const isResultEmpty = (result: Result) => { + const isEmptyArray = Array.isArray(result.data) && result.data.length === 0; + const isEmptyObject = !Array.isArray(result.data) && !result.data; + + return result.status === 'success' && result.data && (isEmptyArray || isEmptyObject); +}; + +const isAnyEmpty = (results: Result | Result[]) => { + return Array.isArray(results) ? results.some(isResultEmpty) : isResultEmpty(results); +}; + +export const QueryEmpty = ({ + children, + result, + message, + isEmpty = isAnyEmpty, + retry = false, + ...rest +}: QueryEmptyProps) => { + if (isEmpty(result)) { + return ( + + ({ color: theme.palette.text.secondary })}> + {message} + + {retry && ( + + )} + + ); + } + + return <>{children}; +}; diff --git a/packages/common/src/client/api/components/QueryEmpty/index.ts b/packages/common/src/client/api/components/QueryEmpty/index.ts new file mode 100644 index 0000000..d009366 --- /dev/null +++ b/packages/common/src/client/api/components/QueryEmpty/index.ts @@ -0,0 +1 @@ +export * from './QueryEmpty'; diff --git a/packages/common/src/client/api/components/QueryError/QueryError.tsx b/packages/common/src/client/api/components/QueryError/QueryError.tsx index fb6126b..07da1a0 100644 --- a/packages/common/src/client/api/components/QueryError/QueryError.tsx +++ b/packages/common/src/client/api/components/QueryError/QueryError.tsx @@ -11,16 +11,16 @@ export interface QueryErrorProps extends Omit message: string; } -function getError(result: QueryErrorProps['result']) { +function hasError(result: QueryErrorProps['result']) { if (Array.isArray(result)) { - return result.find(r => r.error) ?? null; + return result.some(r => r.status === 'error'); } - return result.error ?? null; + return result.status === 'error'; } export const QueryError = ({ result, children = null, ...rest }: QueryErrorProps) => { - if (getError(result)) { + if (hasError(result)) { return ( } {...rest}> {rest.message} diff --git a/packages/common/src/client/api/components/index.ts b/packages/common/src/client/api/components/index.ts index f6d1d99..4543ee7 100644 --- a/packages/common/src/client/api/components/index.ts +++ b/packages/common/src/client/api/components/index.ts @@ -1,3 +1,4 @@ export * from './AppQueryProvider'; +export * from './QueryEmpty'; export * from './QueryError'; export * from './QueryLoader'; diff --git a/packages/common/src/client/clipboard/hooks/useCopyTextToClipboard.ts b/packages/common/src/client/clipboard/hooks/useCopyTextToClipboard.ts index de837a9..fd7c47c 100644 --- a/packages/common/src/client/clipboard/hooks/useCopyTextToClipboard.ts +++ b/packages/common/src/client/clipboard/hooks/useCopyTextToClipboard.ts @@ -1,7 +1,6 @@ import { useMutation } from '@tanstack/react-query'; import { useSnack } from '~client/snackbar/hooks'; -import { logger } from '~logger'; export function useCopyTextToClipboard() { const snack = useSnack(); @@ -16,9 +15,6 @@ export function useCopyTextToClipboard() { await navigator.clipboard.write([clipboardItem]); }, - onError(error) { - logger.error(error); - }, onSuccess: () => { snack('success', 'Copied to clipboard'); }, diff --git a/packages/common/src/client/core/components/App.tsx b/packages/common/src/client/core/components/App.tsx index cac2f63..e1c71c5 100644 --- a/packages/common/src/client/core/components/App.tsx +++ b/packages/common/src/client/core/components/App.tsx @@ -3,6 +3,7 @@ import 'reset.css'; import type { AppProps } from 'next/app'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { NuqsAdapter } from 'nuqs/adapters/next/pages'; import { IntlProvider } from 'react-intl'; import { AppQueryProvider } from '~client/api/components'; @@ -22,7 +23,9 @@ export function App({ Component, pageProps: { dehydratedState, ...pageProps }, e - + + + diff --git a/packages/common/src/client/core/components/Document.tsx b/packages/common/src/client/core/components/Document.tsx index e72bfad..37f4cb5 100644 --- a/packages/common/src/client/core/components/Document.tsx +++ b/packages/common/src/client/core/components/Document.tsx @@ -17,10 +17,8 @@ function MyDocument({ emotionStyleTags }: MyDocumentProps) { name='description' content='A full-stack WebAuthn example of creating a passkey (attestation ceremony) and then retrieving it (assertation ceremony) with Firebase Firestore integration to store the passkey and Firebase Auth for issuing a JWT token.' /> - {/* TODO: */} {/* */} - {/* TODO: */} {/* */} {/* */} {/* */} diff --git a/packages/common/src/client/env/env.mjs b/packages/common/src/client/env/env.mjs index 8858de8..92cac5e 100644 --- a/packages/common/src/client/env/env.mjs +++ b/packages/common/src/client/env/env.mjs @@ -21,7 +21,10 @@ export const env = createEnv({ NEXT_PUBLIC_FIREBASE_APP_ID: z.string(), NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: z.string(), - NEXT_PUBLIC_FIRBEASE_DB_ID: z.string().optional(), + NEXT_PUBLIC_FIREBASE_DB_ID: z.string().optional(), + + NEXT_PUBLIC_DEFAULT_EXAMPLE_ORIGIN: z.string().url(), + NEXT_PUBLIC_UPGRADE_EXAMPLE_ORIGIN: z.string().url(), }, /** @@ -41,12 +44,16 @@ export const env = createEnv({ NEXT_PUBLIC_FIREBASE_APP_ID: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID, - NEXT_PUBLIC_FIRBEASE_DB_ID: process.env.NEXT_PUBLIC_FIRBEASE_DB_ID, + NEXT_PUBLIC_FIREBASE_DB_ID: process.env.NEXT_PUBLIC_FIREBASE_DB_ID, // Dev NEXT_PUBLIC_DEV_RETRY_QUERIES: process.env.NEXT_PUBLIC_DEV_RETRY_QUERIES, NEXT_PUBLIC_DEV_SENTRY_DISABLED: process.env.NEXT_PUBLIC_DEV_SENTRY_DISABLED, + + // Example origins + NEXT_PUBLIC_DEFAULT_EXAMPLE_ORIGIN: process.env.NEXT_PUBLIC_DEFAULT_EXAMPLE_ORIGIN, + NEXT_PUBLIC_UPGRADE_EXAMPLE_ORIGIN: process.env.NEXT_PUBLIC_UPGRADE_EXAMPLE_ORIGIN, }, - skipValidation: !!process.env.CI || !!process.env.SKIP_ENV_VALIDATION || process.env.npm_lifecycle_event === 'lint', + skipValidation: !!process.env.SKIP_ENV_VALIDATION || process.env.npm_lifecycle_event === 'lint', }); diff --git a/packages/common/src/client/webauthn/utils/parseWebAuthnError.ts b/packages/common/src/client/errors/index.ts similarity index 71% rename from packages/common/src/client/webauthn/utils/parseWebAuthnError.ts rename to packages/common/src/client/errors/index.ts index a6c5e9c..3113b30 100644 --- a/packages/common/src/client/webauthn/utils/parseWebAuthnError.ts +++ b/packages/common/src/client/errors/index.ts @@ -1,6 +1,8 @@ import { isAntonioError } from '@ackee/antonio-core'; import { WebAuthnError } from '@simplewebauthn/browser'; +import { parseCustomFirebaseErrorMessage } from '~client/firebase/errors'; + function isAbortError(error: unknown): error is { name: 'AbortError'; message: string; @@ -9,7 +11,16 @@ function isAbortError(error: unknown): error is { return error instanceof Error && error.name === 'AbortError' && 'message' in error && 'stack' in error; } -export async function parseWebAuthnError(error: unknown) { +export async function parseUnknownError(error: unknown) { + const firebaseError = parseCustomFirebaseErrorMessage(error); + + if (firebaseError) { + return { + type: 'FIREBASE', + message: firebaseError, + } as const; + } + if (isAntonioError(error)) { const errroData = error.response.bodyUsed ? error.data : await error.response.text(); @@ -35,6 +46,6 @@ export async function parseWebAuthnError(error: unknown) { return { type: 'UNKNOWN', - message: 'An unknown error occurred', + message: 'An unknown error occurred. Please try it again later.', } as const; } diff --git a/packages/common/src/client/example/components/ExampleAuth/AuthLoader/AuthLoader.tsx b/packages/common/src/client/example/components/ExampleAuth/AuthLoader/AuthLoader.tsx deleted file mode 100644 index 112cf34..0000000 --- a/packages/common/src/client/example/components/ExampleAuth/AuthLoader/AuthLoader.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Loader, LoaderContainer } from '~client/ui-kit'; - -export interface AuthLoaderProps {} - -export const AuthLoader = ({}: AuthLoaderProps) => { - return ( - - - - ); -}; diff --git a/packages/common/src/client/example/components/ExampleAuth/AuthLoader/index.ts b/packages/common/src/client/example/components/ExampleAuth/AuthLoader/index.ts deleted file mode 100644 index a7d942c..0000000 --- a/packages/common/src/client/example/components/ExampleAuth/AuthLoader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './AuthLoader'; diff --git a/packages/common/src/client/example/components/ExampleAuth/AuthProvider/AuthProvider.tsx b/packages/common/src/client/example/components/ExampleAuth/AuthProvider/AuthProvider.tsx deleted file mode 100644 index e6d93bd..0000000 --- a/packages/common/src/client/example/components/ExampleAuth/AuthProvider/AuthProvider.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import { onAuthStateChanged } from 'firebase/auth'; - -import { api } from '~client/api/fetcher'; -import { auth } from '~client/firebase/config'; - -import { AuthContext, type AuthSession } from './contexts'; - -export interface AuthProviderProps { - children: React.ReactNode; - Loader?: React.ComponentType; -} - -export const AuthProvider = ({ children, Loader = () => null }: AuthProviderProps) => { - const [session, setSession] = useState({ - state: 'loading', - authUser: null, - }); - - const interceptorId = useRef(null); - - useEffect(() => { - return onAuthStateChanged(auth(), async user => { - if (user) { - interceptorId.current = api().interceptors.request.use(undefined, async request => { - const idToken = await user.getIdToken(); - - request.headers.set('Authorization', `Bearer ${idToken}`); - - return request; - }); - - setSession({ - authUser: user, - state: 'authenticated', - }); - } else { - if (interceptorId.current) { - api().interceptors.request.eject(interceptorId.current); - } - - setSession({ - authUser: null, - state: 'unauthenticated', - }); - } - }); - }, []); - - if (!session || session.state === 'loading') { - return ; - } - - return {children}; -}; diff --git a/packages/common/src/client/example/components/ExampleAuth/AuthProvider/index.ts b/packages/common/src/client/example/components/ExampleAuth/AuthProvider/index.ts deleted file mode 100644 index 971096f..0000000 --- a/packages/common/src/client/example/components/ExampleAuth/AuthProvider/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './AuthProvider'; diff --git a/packages/common/src/client/example/components/ExampleAuth/ExampleAuth.tsx b/packages/common/src/client/example/components/ExampleAuth/ExampleAuth.tsx index 9b76972..c53d0c2 100644 --- a/packages/common/src/client/example/components/ExampleAuth/ExampleAuth.tsx +++ b/packages/common/src/client/example/components/ExampleAuth/ExampleAuth.tsx @@ -1,12 +1,68 @@ -import type { ReactNode } from 'react'; +import { useEffect, useRef, useState, type ReactNode } from 'react'; +import { onAuthStateChanged, type User } from 'firebase/auth'; -import { AuthLoader } from './AuthLoader'; -import { AuthProvider } from './AuthProvider'; +import { api } from '~client/api/fetcher'; +import { auth } from '~client/firebase/config'; +import { Loader, LoaderContainer } from '~client/ui-kit'; + +import { AuthContext, type AuthSession } from './contexts'; export interface ExampleAuthProps { children: ReactNode; } +const hasOnlyEmailProviderWithUnVerifiedEmail = (user: User) => { + return user.providerData.length === 1 && user.providerData[0].providerId === 'password' && !user.emailVerified; +}; + export const ExampleAuth = ({ children }: ExampleAuthProps) => { - return {children}; + const [session, setSession] = useState({ + state: 'loading', + authUser: null, + }); + + const interceptorId = useRef(null); + + useEffect(() => { + return onAuthStateChanged(auth(), async user => { + if (!user || hasOnlyEmailProviderWithUnVerifiedEmail(user)) { + if (interceptorId.current) { + api().interceptors.request.eject(interceptorId.current); + } + + setSession({ + authUser: null, + state: 'unauthenticated', + }); + + return; + } + + interceptorId.current = api().interceptors.request.use(undefined, async request => { + const idToken = await user.getIdToken(); + + request.headers.set('Authorization', `Bearer ${idToken}`); + + return request; + }); + + const idTokenResult = await user.getIdTokenResult(); + + setSession({ + authUser: user, + tokenClaims: idTokenResult.claims, + state: 'authenticated', + }); + }); + }, []); + + if (!session || session.state === 'loading') { + return ( + + + + ); + } + + return {children}; }; diff --git a/packages/common/src/client/example/components/ExampleAuth/AuthProvider/contexts/index.ts b/packages/common/src/client/example/components/ExampleAuth/contexts/index.ts similarity index 83% rename from packages/common/src/client/example/components/ExampleAuth/AuthProvider/contexts/index.ts rename to packages/common/src/client/example/components/ExampleAuth/contexts/index.ts index 1484d1f..cd760f5 100644 --- a/packages/common/src/client/example/components/ExampleAuth/AuthProvider/contexts/index.ts +++ b/packages/common/src/client/example/components/ExampleAuth/contexts/index.ts @@ -1,5 +1,5 @@ import { createContext, type Dispatch, type SetStateAction } from 'react'; -import type { User } from 'firebase/auth'; +import type { ParsedToken, User } from 'firebase/auth'; export type AuthSession = | { @@ -9,6 +9,7 @@ export type AuthSession = | { state: 'authenticated'; authUser: User; + tokenClaims: ParsedToken; }; export type AuthContextType = Readonly<{ diff --git a/packages/common/src/client/example/components/ExampleAuth/hooks/useAuthTokenClaims.ts b/packages/common/src/client/example/components/ExampleAuth/hooks/useAuthTokenClaims.ts new file mode 100644 index 0000000..99ef9ec --- /dev/null +++ b/packages/common/src/client/example/components/ExampleAuth/hooks/useAuthTokenClaims.ts @@ -0,0 +1,7 @@ +import { useExampleAuthSession } from './useExampleAuthSession'; + +export function useAuthTokenClaims() { + const { session } = useExampleAuthSession(); + + return session.state === 'authenticated' ? session.tokenClaims : null; +} diff --git a/packages/common/src/client/example/components/ExampleAuth/AuthProvider/hooks/useAuthUser.ts b/packages/common/src/client/example/components/ExampleAuth/hooks/useAuthUser.ts similarity index 100% rename from packages/common/src/client/example/components/ExampleAuth/AuthProvider/hooks/useAuthUser.ts rename to packages/common/src/client/example/components/ExampleAuth/hooks/useAuthUser.ts diff --git a/packages/common/src/client/example/components/ExampleAuth/AuthProvider/hooks/useExampleAuthSession.ts b/packages/common/src/client/example/components/ExampleAuth/hooks/useExampleAuthSession.ts similarity index 100% rename from packages/common/src/client/example/components/ExampleAuth/AuthProvider/hooks/useExampleAuthSession.ts rename to packages/common/src/client/example/components/ExampleAuth/hooks/useExampleAuthSession.ts diff --git a/packages/common/src/client/example/components/ExampleAuth/index.ts b/packages/common/src/client/example/components/ExampleAuth/index.ts index c243689..dd3abf5 100644 --- a/packages/common/src/client/example/components/ExampleAuth/index.ts +++ b/packages/common/src/client/example/components/ExampleAuth/index.ts @@ -1,3 +1,4 @@ -export * from './AuthProvider/hooks/useAuthUser'; -export * from './AuthProvider/hooks/useExampleAuthSession'; export * from './ExampleAuth'; +export * from './hooks/useAuthTokenClaims'; +export * from './hooks/useAuthUser'; +export * from './hooks/useExampleAuthSession'; diff --git a/packages/common/src/client/example/components/ExampleFrame/ExampleFrame.styles.ts b/packages/common/src/client/example/components/ExampleFrame/ExampleFrame.styles.ts index 50189dd..ca1c121 100644 --- a/packages/common/src/client/example/components/ExampleFrame/ExampleFrame.styles.ts +++ b/packages/common/src/client/example/components/ExampleFrame/ExampleFrame.styles.ts @@ -3,7 +3,7 @@ import { Box, styled } from '@mui/material'; export const Container = styled(Box)(({ theme }) => ({ display: 'flex', justifyContent: 'center', - marginTop: theme.spacing(6), + marginTop: theme.spacing(4), })); export const Frame = styled(Box, { @@ -23,4 +23,6 @@ export const Frame = styled(Box, { display: 'grid', gridTemplateRows: 'auto 1fr', boxShadow: '0px 8px 25px rgba(0, 0, 0, 0.1)', + overflow: 'hidden', + position: 'relative', })); diff --git a/packages/common/src/client/example/components/ExampleHeader/ExampleHeader.tsx b/packages/common/src/client/example/components/ExampleHeader/ExampleHeader.tsx index 254d451..171519e 100644 --- a/packages/common/src/client/example/components/ExampleHeader/ExampleHeader.tsx +++ b/packages/common/src/client/example/components/ExampleHeader/ExampleHeader.tsx @@ -1,14 +1,19 @@ import type { ReactNode } from 'react'; +import Link from 'next/link'; +import { GitHub } from '@mui/icons-material'; import { Box } from '@mui/material'; +import { Button } from '~client/ui-kit'; + import { Words } from '../../../ui-kit/components/Words'; export interface ExampleHeaderProps { title: ReactNode; description: ReactNode; + githubUrl: string; } -export const ExampleHeader = ({ title, description }: ExampleHeaderProps) => { +export const ExampleHeader = ({ title, description, githubUrl }: ExampleHeaderProps) => { return ( <> @@ -22,6 +27,19 @@ export const ExampleHeader = ({ title, description }: ExampleHeaderProps) => { > {description} + + + ); }; diff --git a/packages/common/src/client/example/components/ExampleRouter/createExampleRouter.tsx b/packages/common/src/client/example/components/ExampleRouter/createExampleRouter.tsx index 43a5849..4c73e2c 100644 --- a/packages/common/src/client/example/components/ExampleRouter/createExampleRouter.tsx +++ b/packages/common/src/client/example/components/ExampleRouter/createExampleRouter.tsx @@ -1,28 +1,27 @@ import { createContext, createElement, useContext, useState, type ReactElement, type ReactNode } from 'react'; +import { Loader, LoaderContainer } from '~client/ui-kit'; + export type Pathname = string; -export type RenderRoute = () => ReactElement; -export type UnknownRoutes = Readonly>; +export type RenderRouteComponent = () => ReactElement; +export type UnknownRoutes = Readonly>; export type ExampleRouterContextValue = { routes: Routes; - currentRoute: keyof Routes; + currentRoute: keyof Routes | null; redirect: (route: keyof Routes) => void; }; export interface ExampleRouterProps { children: ReactNode; - initialRoute: keyof Routes; routes: Routes; } export function createExampleRouter() { - type Route = keyof Routes; - const ExampleRouterContext = createContext | undefined>(undefined); - const ExampleRouter = ({ children, initialRoute, routes }: ExampleRouterProps) => { - const [currentRoute, setCurrentRoute] = useState(initialRoute); + const ExampleRouter = ({ children, routes }: ExampleRouterProps) => { + const [currentRoute, setCurrentRoute] = useState(null); return ( @@ -44,6 +43,14 @@ export function createExampleRouter() { function CurrentExampleRoute() { const { routes, currentRoute } = useExampleRouter(); + if (!currentRoute) { + return ( + + + + ); + } + return createElement(routes[currentRoute]); } diff --git a/packages/common/src/client/firebase/config/index.ts b/packages/common/src/client/firebase/config/index.ts index 7dfb6e2..e7c62e1 100644 --- a/packages/common/src/client/firebase/config/index.ts +++ b/packages/common/src/client/firebase/config/index.ts @@ -8,6 +8,7 @@ import { env } from '~client/env'; let app: ReturnType; export const getFirebaseApp = () => { + // @ts-ignore if (app) { return app; } @@ -28,11 +29,17 @@ export const getFirebaseApp = () => { ); }; -export const auth = () => getAuth(getFirebaseApp()); +export const auth = () => { + const auth = getAuth(getFirebaseApp()); + + auth.useDeviceLanguage(); + + return auth; +}; export const analytics = () => getAnalytics(getFirebaseApp()); export const db = () => { const app = getFirebaseApp(); - const databaseId = env.NEXT_PUBLIC_FIRBEASE_DB_ID; + const databaseId = env.NEXT_PUBLIC_FIREBASE_DB_ID; return databaseId ? getFirestore(app, databaseId) : getFirestore(app); }; diff --git a/packages/common/src/client/firebase/constants/errors.ts b/packages/common/src/client/firebase/constants/errors.ts new file mode 100644 index 0000000..9a26130 --- /dev/null +++ b/packages/common/src/client/firebase/constants/errors.ts @@ -0,0 +1,9 @@ +import type { FirebaseAuthErrorCodeKey } from '../errors'; + +export const customFirebaseErrors = { + 'auth/email-already-in-use': 'The email address is already in use by another account.', + 'auth/app-deleted': 'The user corresponding to the provided email has been deleted.', + 'auth/user-disabled': 'The user corresponding to the provided email has been disabled.', +} as const satisfies Partial>; + +export type CustomFirebaseErrors = typeof customFirebaseErrors; diff --git a/packages/common/src/client/firebase/constants/index.ts b/packages/common/src/client/firebase/constants/index.ts new file mode 100644 index 0000000..f72bc43 --- /dev/null +++ b/packages/common/src/client/firebase/constants/index.ts @@ -0,0 +1 @@ +export * from './errors'; diff --git a/packages/common/src/client/firebase/errors/index.ts b/packages/common/src/client/firebase/errors/index.ts new file mode 100644 index 0000000..b4cc798 --- /dev/null +++ b/packages/common/src/client/firebase/errors/index.ts @@ -0,0 +1,38 @@ +import { FirebaseError } from 'firebase/app'; +import { AuthErrorCodes } from 'firebase/auth'; + +import { customFirebaseErrors, type CustomFirebaseErrors } from '../constants'; + +const isFirebaseError = (error: unknown): error is FirebaseError => error instanceof FirebaseError; + +export type FirebaseAuthErrorCodes = typeof AuthErrorCodes; + +export type FirebaseAuthErrorCodeKey = FirebaseAuthErrorCodes[keyof FirebaseAuthErrorCodes]; + +const errorCodes = new Set(Object.keys(AuthErrorCodes)) as Set; + +/** + * @url https://firebase.google.com/docs/reference/js/v8/firebase.auth.Error + */ +export const isFirebaseErrorWithCodes = ( + error: unknown, + codes: Codes, +): error is FirebaseError & { code: FirebaseAuthErrorCodes[(typeof codes)[number]] } => { + return ( + isFirebaseError(error) && + codes.every(code => errorCodes.has(code)) && + Object.values(AuthErrorCodes).includes(error.code) + ); +}; + +export const parseCustomFirebaseErrorMessage = (error: unknown) => { + const codes = Object.keys(customFirebaseErrors) as (keyof CustomFirebaseErrors)[]; + + if (isFirebaseError(error) && codes.includes(error.code)) { + const code = error.code as keyof CustomFirebaseErrors; + + return customFirebaseErrors[code]; + } + + return null; +}; diff --git a/packages/common/src/client/form/components/Form/Form.tsx b/packages/common/src/client/form/components/Form/Form.tsx index 4478052..dfd96ee 100644 --- a/packages/common/src/client/form/components/Form/Form.tsx +++ b/packages/common/src/client/form/components/Form/Form.tsx @@ -9,6 +9,7 @@ export interface FormProps) => Promise | void; defaultValues?: UseFormProps['defaultValues']; + mode?: UseFormProps['mode']; } export const Form = ({ @@ -16,12 +17,13 @@ export const Form = ) => { const resolver = useLocalizedResolver(schema); const form = useForm({ resolver, defaultValues, - mode: 'onSubmit', + mode, }); const submit = useMemo(() => form.handleSubmit(values => onSubmit(values, form)), [form, onSubmit]); diff --git a/packages/common/src/client/form/errors/index.ts b/packages/common/src/client/form/errors/index.ts index c43d53e..a3f45e2 100644 --- a/packages/common/src/client/form/errors/index.ts +++ b/packages/common/src/client/form/errors/index.ts @@ -1,4 +1,10 @@ export const formError = { required: 'Required', email: 'Please enter valid e-mail address.', + password: { + length: 'Inserted password must be at least 10 chars of length.', + casing: 'Inserted password must contain at least one capital letter.', + special: 'Inserted password must contain at least 1 special character.', + numerical: 'Inserted password must contain at least 1 number.', + }, } as const; diff --git a/packages/common/src/client/form/validators/password.ts b/packages/common/src/client/form/validators/password.ts index 26c13c9..7be0c51 100644 --- a/packages/common/src/client/form/validators/password.ts +++ b/packages/common/src/client/form/validators/password.ts @@ -1,5 +1,25 @@ import { z } from 'zod'; +import { formError } from '../errors'; import { required } from './general'; -export const newPassword = required.pipe(z.string().min(10)); +const includesOneDigits = /(?:\D*\d){1}/; +const includesOneUpperCase = /(?=.*[A-Z])/; +const includesOneSpecialChar = /[A-Za-z0-9 ]*[^A-Za-z0-9 ][A-Za-z0-9 ]*/; + +export const newPassword = required.pipe( + z + .string() + .min(10, formError.password.length) + .refine(value => includesOneDigits.test(value), { + message: formError.password.numerical, + }) + .refine(value => includesOneUpperCase.test(value), { + message: formError.password.casing, + }) + .refine(value => includesOneSpecialChar.test(value), { + message: formError.password.special, + }), +); + +export const password = required; diff --git a/packages/common/src/client/layout/components/MainHeader/MainHeader.tsx b/packages/common/src/client/layout/components/MainHeader/MainHeader.tsx deleted file mode 100644 index a4b9097..0000000 --- a/packages/common/src/client/layout/components/MainHeader/MainHeader.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import Head from 'next/head'; - -import { PageHeader } from '~client/ui-kit'; - -export interface MainHeaderProps { - pageTitle: string; -} - -export const MainHeader = ({ pageTitle }: MainHeaderProps) => { - return ( - <> - - {`${pageTitle} | With WebAuthn`} - - With WebAuthn demo - - ); -}; diff --git a/packages/common/src/client/layout/components/MainHeader/index.ts b/packages/common/src/client/layout/components/MainHeader/index.ts deleted file mode 100644 index 3d8a192..0000000 --- a/packages/common/src/client/layout/components/MainHeader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './MainHeader'; diff --git a/packages/common/src/client/layout/components/PageDrawer/PageDrawer.styles.ts b/packages/common/src/client/layout/components/PageDrawer/PageDrawer.styles.ts new file mode 100644 index 0000000..cc2f28f --- /dev/null +++ b/packages/common/src/client/layout/components/PageDrawer/PageDrawer.styles.ts @@ -0,0 +1,5 @@ +import { Box, styled } from '~client/ui-kit'; + +export const DrawerContent = styled(Box)(({ theme }) => ({ + width: 375, +})); diff --git a/packages/common/src/client/layout/components/PageDrawer/PageDrawer.tsx b/packages/common/src/client/layout/components/PageDrawer/PageDrawer.tsx new file mode 100644 index 0000000..8781b98 --- /dev/null +++ b/packages/common/src/client/layout/components/PageDrawer/PageDrawer.tsx @@ -0,0 +1,59 @@ +import { useState } from 'react'; +import Link from 'next/link'; +import { Close, Menu } from '@mui/icons-material'; +import { Divider, Drawer, IconButton, List, ListItem, ListItemButton, Stack } from '@mui/material'; + +import { env } from '~client/env'; +import { Words } from '~client/ui-kit'; + +import { DrawerContent } from './PageDrawer.styles'; + +export const PageDrawer = () => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(!open)} size='large' aria-label='menu'> + + + setOpen(false)}> + + + + WebAuthn Demos + + + setOpen(false)} size='large' aria-label='close'> + + + + + + + { + setOpen(false); + }} + > + Authenticate with passkeys + + + + { + setOpen(false); + }} + > + Upgrade to passkeys + + + + + + + ); +}; diff --git a/packages/common/src/client/layout/components/PageDrawer/index.ts b/packages/common/src/client/layout/components/PageDrawer/index.ts new file mode 100644 index 0000000..031897d --- /dev/null +++ b/packages/common/src/client/layout/components/PageDrawer/index.ts @@ -0,0 +1 @@ +export * from './PageDrawer'; diff --git a/packages/common/src/client/layout/components/PageHeader/PageHeader.styles.ts b/packages/common/src/client/layout/components/PageHeader/PageHeader.styles.ts new file mode 100644 index 0000000..a92af4d --- /dev/null +++ b/packages/common/src/client/layout/components/PageHeader/PageHeader.styles.ts @@ -0,0 +1,32 @@ +import { Container, Icon, styled, Words } from '~client/ui-kit'; + +export const StyledContainer = styled(Container)(({ theme }) => ({ + paddingTop: theme.spacing(3), + paddingBottom: theme.spacing(3), + paddingLeft: `${theme.spacing(1)} !important`, + paddingRight: `${theme.spacing(1)} !important`, + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', +})); + +export const LeftSide = styled('div')(({ theme }) => ({ + display: 'grid', + gridTemplateColumns: 'auto auto', + gap: theme.spacing(1), + alignItems: 'center', +})); + +export const PageTitle = styled(Words)(({ theme }) => ({ + fontSize: '1.75rem', + + [theme.breakpoints.down('sm')]: { + fontSize: theme.typography.h3.fontSize, + }, +})); + +export const IconGithub = styled(Icon)(({ theme }) => ({ + color: '#000', + height: 32, + width: 32, +})); diff --git a/packages/common/src/client/layout/components/PageHeader/PageHeader.tsx b/packages/common/src/client/layout/components/PageHeader/PageHeader.tsx new file mode 100644 index 0000000..e36a516 --- /dev/null +++ b/packages/common/src/client/layout/components/PageHeader/PageHeader.tsx @@ -0,0 +1,37 @@ +import Head from 'next/head'; +import Link from 'next/link'; +import { GitHub } from '@mui/icons-material'; + +import { PageDrawer } from '../PageDrawer'; +import { IconGithub, LeftSide, PageTitle, StyledContainer } from './PageHeader.styles'; + +export interface PageHeaderProps { + pageTitle: string; +} + +export const PageHeader = ({ pageTitle }: PageHeaderProps) => { + return ( + <> + + {`${pageTitle} | With WebAuthn`} + + + + + With WebAuthn demos + + + + + + + ); +}; diff --git a/packages/common/src/client/ui-kit/components/PageHeader/index.ts b/packages/common/src/client/layout/components/PageHeader/index.ts similarity index 100% rename from packages/common/src/client/ui-kit/components/PageHeader/index.ts rename to packages/common/src/client/layout/components/PageHeader/index.ts diff --git a/packages/common/src/client/layout/components/index.ts b/packages/common/src/client/layout/components/index.ts index 3d8a192..ba434ed 100644 --- a/packages/common/src/client/layout/components/index.ts +++ b/packages/common/src/client/layout/components/index.ts @@ -1 +1 @@ -export * from './MainHeader'; +export * from './PageHeader'; diff --git a/packages/common/src/client/passkeys/components/PasskeysHeader/PasskeysHeader.styles.ts b/packages/common/src/client/passkeys/components/PasskeysHeader/PasskeysHeader.styles.ts new file mode 100644 index 0000000..4bce513 --- /dev/null +++ b/packages/common/src/client/passkeys/components/PasskeysHeader/PasskeysHeader.styles.ts @@ -0,0 +1,10 @@ +import { styled } from '~client/ui-kit'; + +export const Header = styled('header')(({ theme }) => ({ + marginBottom: theme.spacing(2), + marginTop: theme.spacing(2), + display: 'grid', + justifyContent: 'space-between', + alignItems: 'center', + gridTemplateColumns: 'auto auto', +})); diff --git a/packages/common/src/client/passkeys/components/PasskeysHeader/PasskeysHeader.tsx b/packages/common/src/client/passkeys/components/PasskeysHeader/PasskeysHeader.tsx new file mode 100644 index 0000000..d763a7c --- /dev/null +++ b/packages/common/src/client/passkeys/components/PasskeysHeader/PasskeysHeader.tsx @@ -0,0 +1,28 @@ +import { Fingerprint } from '@mui/icons-material'; +import type { UseMutationResult } from '@tanstack/react-query'; + +import { Button, Words } from '~client/ui-kit'; + +import { Header } from './PasskeysHeader.styles'; + +export interface PasskeysHeaderProps { + addPasskey: UseMutationResult; +} + +export const PasskeysHeader = ({ addPasskey }: PasskeysHeaderProps) => { + return ( +
+ Passkeys + +
+ ); +}; diff --git a/packages/common/src/client/passkeys/components/PasskeysHeader/index.ts b/packages/common/src/client/passkeys/components/PasskeysHeader/index.ts new file mode 100644 index 0000000..cde5059 --- /dev/null +++ b/packages/common/src/client/passkeys/components/PasskeysHeader/index.ts @@ -0,0 +1 @@ +export * from './PasskeysHeader'; diff --git a/packages/common/src/client/passkeys/components/PasskeysList/PasskeysList.styles.ts b/packages/common/src/client/passkeys/components/PasskeysList/PasskeysList.styles.ts new file mode 100644 index 0000000..63062d7 --- /dev/null +++ b/packages/common/src/client/passkeys/components/PasskeysList/PasskeysList.styles.ts @@ -0,0 +1,6 @@ +import { styled } from '@mui/material'; + +export const List = styled('section')(({ theme }) => ({ + display: 'grid', + rowGap: theme.spacing(3.5), +})); diff --git a/packages/common/src/client/passkeys/components/PasskeysList/PasskeysList.tsx b/packages/common/src/client/passkeys/components/PasskeysList/PasskeysList.tsx new file mode 100644 index 0000000..c8152b6 --- /dev/null +++ b/packages/common/src/client/passkeys/components/PasskeysList/PasskeysList.tsx @@ -0,0 +1,36 @@ +import { QueryEmpty, QueryError, QueryLoader } from '~client/api/components'; + +import { Passkey, type PasskeyProps } from '../Passkey/Passkey'; +import { useFetchPasskeys } from './hooks/useFetchPasskeys'; +import { List } from './PasskeysList.styles'; + +export interface PasskeysListProps { + removePasskey: PasskeyProps['removePasskey']; + isPasskeyRemovable?: (index: number) => boolean; +} + +export const PasskeysList = ({ removePasskey, isPasskeyRemovable = index => index > 0 }: PasskeysListProps) => { + const passkeysResult = useFetchPasskeys(); + + return ( + + + + + {passkeysResult.data.map((passkey, index) => ( + + ))} + + + + + ); +}; diff --git a/packages/common/src/client/passkeys/components/PasskeysList/hooks/useFetchPasskeys.ts b/packages/common/src/client/passkeys/components/PasskeysList/hooks/useFetchPasskeys.ts new file mode 100644 index 0000000..778593d --- /dev/null +++ b/packages/common/src/client/passkeys/components/PasskeysList/hooks/useFetchPasskeys.ts @@ -0,0 +1,16 @@ +import { useQuery } from '@tanstack/react-query'; + +import { useAuthUser } from '~client/example/components'; +import { fetchUserPasskeys } from '~client/firebase/services/passkeys'; + +export function useFetchPasskeys() { + const authUser = useAuthUser(); + const uid = authUser?.uid; + + return useQuery({ + queryKey: ['passkeys', uid], + queryFn: () => fetchUserPasskeys(uid!), + initialData: [], + enabled: Boolean(uid), + }); +} diff --git a/packages/common/src/client/passkeys/components/PasskeysList/index.ts b/packages/common/src/client/passkeys/components/PasskeysList/index.ts new file mode 100644 index 0000000..9419dfb --- /dev/null +++ b/packages/common/src/client/passkeys/components/PasskeysList/index.ts @@ -0,0 +1 @@ +export * from './PasskeysList'; diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/PostRemovalDialog/PostRemovalDialog.styles.ts b/packages/common/src/client/passkeys/components/PostRemovalDialog/PostRemovalDialog.styles.ts similarity index 82% rename from examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/PostRemovalDialog/PostRemovalDialog.styles.ts rename to packages/common/src/client/passkeys/components/PostRemovalDialog/PostRemovalDialog.styles.ts index 14403f3..5f34d73 100644 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/PostRemovalDialog/PostRemovalDialog.styles.ts +++ b/packages/common/src/client/passkeys/components/PostRemovalDialog/PostRemovalDialog.styles.ts @@ -1,4 +1,4 @@ -import { styled } from '@workspace/common/client/ui-kit'; +import { styled } from '~client/ui-kit'; export const PasskeyDetails = styled('div')(({ theme }) => ({ display: 'grid', diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/PostRemovalDialog/PostRemovalDialog.tsx b/packages/common/src/client/passkeys/components/PostRemovalDialog/PostRemovalDialog.tsx similarity index 84% rename from examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/PostRemovalDialog/PostRemovalDialog.tsx rename to packages/common/src/client/passkeys/components/PostRemovalDialog/PostRemovalDialog.tsx index 61e658a..453773b 100644 --- a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/PostRemovalDialog/PostRemovalDialog.tsx +++ b/packages/common/src/client/passkeys/components/PostRemovalDialog/PostRemovalDialog.tsx @@ -1,9 +1,10 @@ -import { Caption } from '@workspace/common/client/passkeys/components/Passkey/Caption'; -import { ProviderProfile } from '@workspace/common/client/passkeys/components/Passkey/ProviderProfile'; -import { Box, Button, Dialog, DialogActions, Icon, Stack, Words } from '@workspace/common/client/ui-kit'; -import { Warning } from '@workspace/common/client/ui-kit/icons'; -import type { Passkey } from '@workspace/common/types'; +import { Warning } from '@mui/icons-material'; +import { Box, Button, Dialog, DialogActions, Icon, Stack, Words } from '~client/ui-kit'; +import type { Passkey } from '~types'; + +import { Caption } from '../Passkey/Caption'; +import { ProviderProfile } from '../Passkey/ProviderProfile'; import { PasskeyDetails } from './PostRemovalDialog.styles'; export interface PostRemovalDialogProps { diff --git a/examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/PostRemovalDialog/index.ts b/packages/common/src/client/passkeys/components/PostRemovalDialog/index.ts similarity index 100% rename from examples/webauthn-default/src/components/WebAuthnDefaultExamplePage/DefaultExample/Passkeys/PostRemovalDialog/index.ts rename to packages/common/src/client/passkeys/components/PostRemovalDialog/index.ts diff --git a/packages/common/src/client/passkeys/components/index.ts b/packages/common/src/client/passkeys/components/index.ts index 94c8d58..f1482f1 100644 --- a/packages/common/src/client/passkeys/components/index.ts +++ b/packages/common/src/client/passkeys/components/index.ts @@ -1 +1,4 @@ export * from './Passkey'; +export * from './PasskeysHeader'; +export * from './PasskeysList'; +export * from './PostRemovalDialog'; diff --git a/packages/common/src/client/ui-kit/components/Button/Button.styles.ts b/packages/common/src/client/ui-kit/components/Button/Button.styles.ts new file mode 100644 index 0000000..eecc210 --- /dev/null +++ b/packages/common/src/client/ui-kit/components/Button/Button.styles.ts @@ -0,0 +1,26 @@ +import { LoadingButton } from '@mui/lab'; +import { styled } from '@mui/material'; + +export const StyledButton = styled(LoadingButton)(({ theme, color, variant, size }) => ({ + textTransform: 'none', + boxShadow: 'none', + fontSize: '1rem', + fontWeight: '500', + + ...(size === 'large' && { + height: 48, + }), + + ...(color === 'secondary' && + variant === 'outlined' && { + color: theme.palette.text.secondary, + backgroundColor: theme.palette.common.white, + borderColor: theme.palette.text.secondary, + + '&:hover': { + color: theme.palette.text.primary, + backgroundColor: theme.palette.common.white, + borderColor: theme.palette.text.primary, + }, + }), +})); diff --git a/packages/common/src/client/ui-kit/components/Button/Button.tsx b/packages/common/src/client/ui-kit/components/Button/Button.tsx index 45af374..900b83a 100644 --- a/packages/common/src/client/ui-kit/components/Button/Button.tsx +++ b/packages/common/src/client/ui-kit/components/Button/Button.tsx @@ -1,23 +1,13 @@ -import { LoadingButton, type LoadingButtonProps } from '@mui/lab'; +import type { LoadingButtonProps } from '@mui/lab'; + +import { StyledButton } from './Button.styles'; export interface ButtonProps extends LoadingButtonProps {} export const Button = ({ children, ...props }: ButtonProps) => { return ( - + {children} - + ); }; diff --git a/packages/common/src/client/ui-kit/components/PageHeader/PageHeader.tsx b/packages/common/src/client/ui-kit/components/PageHeader/PageHeader.tsx deleted file mode 100644 index 532e9b9..0000000 --- a/packages/common/src/client/ui-kit/components/PageHeader/PageHeader.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import Link from 'next/link'; -import { GitHub } from '@mui/icons-material'; -import { Container } from '@mui/material'; - -import { Icon } from '../Icon'; -import { Words } from '../Words'; - -export interface PageHeaderProps { - children?: React.ReactNode; -} - -export const PageHeader = ({ children }: PageHeaderProps) => { - return ( - ({ - padding: theme.spacing(3, 1.5), - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - })} - component='header' - > - ({ - [theme.breakpoints.down('sm')]: { - fontSize: theme.typography.h3.fontSize, - }, - })} - > - {children} - - - - - - ); -}; diff --git a/packages/common/src/client/ui-kit/components/Words/Words.tsx b/packages/common/src/client/ui-kit/components/Words/Words.tsx index c963908..c92358c 100644 --- a/packages/common/src/client/ui-kit/components/Words/Words.tsx +++ b/packages/common/src/client/ui-kit/components/Words/Words.tsx @@ -1,11 +1,9 @@ -import type { ComponentProps, ElementType } from 'react'; +import type { ElementType } from 'react'; import { Typography, type TypographyProps } from '@mui/material'; type WordOwnProps = {}; -export type WordsProps = WordOwnProps & - TypographyProps & - Omit['component']>, keyof WordOwnProps>; +export type WordsProps = WordOwnProps & TypographyProps; export const Words = ({ children, ...props }: WordsProps) => { return {children}; diff --git a/packages/common/src/client/ui-kit/components/index.ts b/packages/common/src/client/ui-kit/components/index.ts index 027bdae..47d2746 100644 --- a/packages/common/src/client/ui-kit/components/index.ts +++ b/packages/common/src/client/ui-kit/components/index.ts @@ -7,5 +7,4 @@ export * from './Icon'; export * from './InfoTooltip'; export * from './Loader'; export * from './MenuItem'; -export * from './PageHeader'; export * from './Words'; diff --git a/packages/common/src/client/webauthn/utils/index.ts b/packages/common/src/client/webauthn/utils/index.ts deleted file mode 100644 index 9e181e1..0000000 --- a/packages/common/src/client/webauthn/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './parseWebAuthnError'; diff --git a/packages/common/src/server/services/auth/index.ts b/packages/common/src/server/services/auth/index.ts deleted file mode 100644 index c3b280b..0000000 --- a/packages/common/src/server/services/auth/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Auth } from 'firebase-admin/auth'; - -import { auth } from '~server/config/firebase'; - -export async function createAuthUser(props: Parameters[0]) { - await auth().createUser(props); -} - -/** - * @param userId - * @docs https://firebase.google.com/docs/auth/admin/create-custom-tokens - */ -export async function createCustomToken(userId: string) { - return auth().createCustomToken(userId); -} diff --git a/packages/common/src/server/services/challenge-session/index.ts b/packages/common/src/server/services/challenge-session/index.ts index 8295c32..2851104 100644 --- a/packages/common/src/server/services/challenge-session/index.ts +++ b/packages/common/src/server/services/challenge-session/index.ts @@ -10,7 +10,7 @@ import { deleteChallengeSession, getChallengeSession, setChallengeSession } from export type InitializeChallengeSessionProps = { timeout: number; - challenge: ArrayBuffer; + challenge: Uint8Array; } & ( | { type: 'attestation'; @@ -39,6 +39,7 @@ export async function initializeChallengeSession( await setChallengeSession({ ...restProps, id: sessionId, + // @ts-ignore challenge: bufferToBase64URLString(challenge), expiresAt: expiresAt.toISOString(), origin: env.NEXT_PUBLIC_CLIENT_ORIGIN, diff --git a/packages/common/src/server/services/users/index.ts b/packages/common/src/server/services/users/index.ts index 8967b3c..16ca388 100644 --- a/packages/common/src/server/services/users/index.ts +++ b/packages/common/src/server/services/users/index.ts @@ -1,12 +1,11 @@ import { bufferToBase64URLString } from '@simplewebauthn/browser'; import type { VerifiedRegistrationResponse } from '@simplewebauthn/server'; -import { db } from '~server/config/firebase'; +import { auth, db } from '~server/config/firebase'; import { collections } from '~server/constants/collections'; import type { Passkey, User } from '~types'; import { getPasskeyProvider } from '../aaguid'; -import { createAuthUser } from '../auth'; import { addPasskey, getPasskeys, removePasskey, type AddPasskeyProps } from '../passkeys'; export async function findUserByUsername(username: User['username']) { @@ -40,6 +39,17 @@ export async function addUser( return user; } +export async function createUserWithNoPasskeys(uid: string, email: string) { + await db() + .collection(collections.Users) + .doc(uid) + .set({ + id: uid, + username: email, + passkeyIds: [], + } satisfies User); +} + export async function updateUser(userId: string, data: Partial) { await db().collection(collections.Users).doc(userId).update(data); } @@ -60,6 +70,7 @@ function mapPropsToPasskey(userId: string, verifiedRegistrationInfo: VerifiedReg return { credentialId: credential.id, credentialBackedUp, + // @ts-ignore credentialPublicKey: bufferToBase64URLString(credential.publicKey), credentialDeviceType, credentialCounter: credential.counter, @@ -87,7 +98,7 @@ export async function createUserPasskey( }); // Creates a new user in Firebase Authentication. - await createAuthUser({ + await auth().createUser({ uid: userId, email: username, }); diff --git a/packages/common/src/server/utils/parseAndVerifyIdToken.ts b/packages/common/src/server/utils/parseAndVerifyIdToken.ts index 7ea99a3..2df1da6 100644 --- a/packages/common/src/server/utils/parseAndVerifyIdToken.ts +++ b/packages/common/src/server/utils/parseAndVerifyIdToken.ts @@ -1,18 +1,15 @@ -import { logger } from '~logger'; -import { auth } from '~server/config/firebase'; - -export async function parseAndVerifyIdToken(authorizationHeader: string | undefined) { - try { - const idToken = authorizationHeader?.split('Bearer ')[1]; +import type { DecodedIdToken } from 'firebase-admin/auth'; - if (!idToken) { - return null; - } +import { auth } from '~server/config/firebase'; - return await auth().verifyIdToken(idToken, true); - } catch (error) { - logger.error(error); +export async function parseAndVerifyIdToken( + authorizationHeader: string | undefined, +): Promise<(DecodedIdToken & CustomClaims) | null> { + const idToken = authorizationHeader?.split('Bearer ')[1]; + if (!idToken) { return null; } + + return (await auth().verifyIdToken(idToken, true)) as DecodedIdToken & CustomClaims; } diff --git a/packages/common/tsconfig.json b/packages/common/tsconfig.json index 1c268a9..6122c65 100644 --- a/packages/common/tsconfig.json +++ b/packages/common/tsconfig.json @@ -1,10 +1,10 @@ { "extends": "@tooling/typescript/nextjs", "compilerOptions": { - "baseUrl": "./src", + "baseUrl": ".", "paths": { "~*": [ - "./*" + "./src/*" ], }, "outDir": "dist", @@ -12,13 +12,14 @@ "declarationMap": true, "declarationDir": "dist", "noEmit": false, + "incremental": true, + "tsBuildInfoFile": ".cache/.tsbuildinfo" }, "include": [ - "./src/**/*.ts", - "./src/**/*.tsx", + "./src/**/*", ], "exclude": [ "dist", "node_modules" - ], + ] } \ No newline at end of file diff --git a/packages/logger/index.ts b/packages/logger/index.ts index 232e72f..4d1c117 100644 --- a/packages/logger/index.ts +++ b/packages/logger/index.ts @@ -1,4 +1,4 @@ -import { captureException, setExtras } from '@sentry/browser'; +import { captureException, setExtras } from '@sentry/nextjs'; import type { Extras } from '@sentry/types'; import { getLogger } from 'loglevel'; diff --git a/packages/logger/package.json b/packages/logger/package.json index 400c757..c06fd14 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -3,11 +3,11 @@ "version": "0.0.1", "type": "module", "peerDependencies": { + "@sentry/nextjs": "8.x", "next": "15.x", "react": "18.x" }, "dependencies": { - "@sentry/browser": "8.37.1", "loglevel": "1.9.2" }, "devDependencies": { diff --git a/packages/sentry/client.ts b/packages/sentry/client.ts new file mode 100644 index 0000000..d1e1b6b --- /dev/null +++ b/packages/sentry/client.ts @@ -0,0 +1,33 @@ +// This file configures the initialization of Sentry on the client. +// The config you add here will be used whenever a users loads a page in their browser. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import { feedbackIntegration, init, replayIntegration } from '@sentry/nextjs'; + +export function initSentryForClient(dsn: string) { + init({ + dsn, + + // Add optional integrations for additional features + integrations: [ + replayIntegration(), + feedbackIntegration({ + colorScheme: 'system', + }), + ], + + // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. + tracesSampleRate: 1, + + // Define how likely Replay events are sampled. + // This sets the sample rate to be 10%. You may want this to be 100% while + // in development and sample at a lower rate in production + replaysSessionSampleRate: 0.1, + + // Define how likely Replay events are sampled when an error occurs. + replaysOnErrorSampleRate: 1.0, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + }); +} diff --git a/packages/sentry/edge.ts b/packages/sentry/edge.ts new file mode 100644 index 0000000..1847c2e --- /dev/null +++ b/packages/sentry/edge.ts @@ -0,0 +1,17 @@ +// This file configures the initialization of Sentry on the server. +// The config you add here will be used whenever the server handles a request. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import { init } from '@sentry/nextjs'; + +export function initSentryForEdge(dsn: string) { + init({ + dsn, + + // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. + tracesSampleRate: 1, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + }); +} diff --git a/packages/sentry/eslint.config.mjs b/packages/sentry/eslint.config.mjs new file mode 100644 index 0000000..5355fa5 --- /dev/null +++ b/packages/sentry/eslint.config.mjs @@ -0,0 +1,3 @@ +import { nextjsConfig } from '@tooling/eslint/config'; + +export default nextjsConfig; diff --git a/packages/sentry/next-config.ts b/packages/sentry/next-config.ts new file mode 100644 index 0000000..eb02c5a --- /dev/null +++ b/packages/sentry/next-config.ts @@ -0,0 +1,51 @@ +/* eslint-disable no-console */ +import { withSentryConfig } from '@sentry/nextjs'; + +export function withDefinedSentryConfig(nextConfig: C) { + console.log('Sentry is enabled'); + console.log('Sentry organization:', process.env.SENTRY_ORG); + console.log('Sentry project:', process.env.SENTRY_PROJECT); + + return withSentryConfig(nextConfig, { + // For all available options, see: + // https://github.com/getsentry/sentry-webpack-plugin#options + + org: process.env.SENTRY_ORG, + project: process.env.SENTRY_PROJECT, + authToken: process.env.SENTRY_AUTH_TOKEN, + + // Only print logs for uploading source maps in CI + silent: !process.env.CI, + + // For all available options, see: + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, + + // Automatically annotate React components to show their full name in breadcrumbs and session replay + reactComponentAnnotation: { + enabled: true, + }, + + // Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. + // This can increase your server load as well as your hosting bill. + // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- + // side errors will fail. + // tunnelRoute: "/monitoring", + + // Hides source maps from generated client bundles + hideSourceMaps: true, + + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true, + + // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) + // See the following for more information: + // https://docs.sentry.io/product/crons/ + // https://vercel.com/docs/cron-jobs + automaticVercelMonitors: true, + + telemetry: false, + }); +} diff --git a/packages/sentry/package.json b/packages/sentry/package.json new file mode 100644 index 0000000..10a886b --- /dev/null +++ b/packages/sentry/package.json @@ -0,0 +1,21 @@ +{ + "name": "@workspace/sentry", + "version": "0.0.1", + "type": "module", + "dependencies": { + "@sentry/nextjs": "8.41.0", + "next": "15.0.3", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@tooling/eslint": "workspace:*", + "@tooling/prettier": "workspace:*", + "@tooling/typescript": "workspace:*" + }, + "prettier": "@tooling/prettier/config", + "scripts": { + "format": "prettier-format --write ./*.ts", + "lint": "eslint-lint --config eslint.config.mjs ./**/*.ts" + } +} diff --git a/packages/sentry/server.ts b/packages/sentry/server.ts new file mode 100644 index 0000000..52f8e6d --- /dev/null +++ b/packages/sentry/server.ts @@ -0,0 +1,17 @@ +// This file configures the initialization of Sentry on the server. +// The config you add here will be used whenever the server handles a request. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import { init } from '@sentry/nextjs'; + +export function initSentryForServer(dsn: string) { + init({ + dsn, + + // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. + tracesSampleRate: 1, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + }); +} diff --git a/packages/sentry/tsconfig.json b/packages/sentry/tsconfig.json new file mode 100644 index 0000000..80d5d09 --- /dev/null +++ b/packages/sentry/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@tooling/typescript/nextjs", + "include": [ + "**/*.ts" + ] +} \ No newline at end of file diff --git a/tooling/eslint/package.json b/tooling/eslint/package.json index f530f4c..9f6be7b 100644 --- a/tooling/eslint/package.json +++ b/tooling/eslint/package.json @@ -12,24 +12,24 @@ "format": "prettier-format --write ./**/*.js" }, "dependencies": { - "@eslint/eslintrc": "3.1.0", - "@eslint/js": "9.14.0", - "@tanstack/eslint-plugin-query": "5.59.20", + "@eslint/eslintrc": "3.2.0", + "@eslint/js": "9.15.0", + "@tanstack/eslint-plugin-query": "5.61.4", "@tooling/typescript": "workspace:*", "@types/eslint__eslintrc": "2.1.2", "@types/eslint__js": "8.42.3", - "eslint": "9.14.0", + "eslint": "9.15.0", "eslint-config-next": "15.0.3", "eslint-config-prettier": "9.1.0", - "eslint-plugin-turbo": "2.2.3", + "eslint-plugin-turbo": "2.3.3", "next": "15.0.3", "react": "18.3.1", "react-dom": "18.3.1", - "typescript": "5.6.3", - "typescript-eslint": "8.13.0" + "typescript": "5.7.2", + "typescript-eslint": "8.16.0" }, "devDependencies": { - "@eslint/compat": "1.2.2", + "@eslint/compat": "1.2.3", "@tooling/prettier": "workspace:*", "@tooling/staged": "workspace:^", "@types/eslint": "9.6.1" diff --git a/tooling/prettier/package.json b/tooling/prettier/package.json index 69e96cc..45bca07 100644 --- a/tooling/prettier/package.json +++ b/tooling/prettier/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@ianvs/prettier-plugin-sort-imports": "4.4.0", - "prettier": "3.3.3" + "prettier": "3.4.1" }, "devDependencies": { "@tooling/staged": "workspace:^", diff --git a/tooling/typescript/package.json b/tooling/typescript/package.json index 33ea1f3..8f459b7 100644 --- a/tooling/typescript/package.json +++ b/tooling/typescript/package.json @@ -8,8 +8,8 @@ }, "dependencies": { "@total-typescript/ts-reset": "0.6.1", - "@types/node": "20.17.6", + "@types/node": "22.10.1", "network-information-types": "0.1.1", - "typescript": "5.6.3" + "typescript": "5.7.2" } } diff --git a/turbo.json b/turbo.json index 82254d6..79a1ddd 100644 --- a/turbo.json +++ b/turbo.json @@ -1,13 +1,15 @@ { "$schema": "https://turbo.build/schema.json", + "cacheDir": ".cache/turbo", + "ui": "stream", + "globalDependencies": [".env.local", ".env", ".env.server", ".env.sentry-build-plugin"], "tasks": { "test:ci": { "dependsOn": ["//#audit", "build"] }, "build": { - "outputs": ["dist/**", ".next/**", "!.next/cache/**"], - "dependsOn": ["cir-dep", "format", "lint", "^build"], - "inputs": ["$TURBO_DEFAULT$", ".env.local", ".env", ".env.server"] + "outputs": ["dist/**", ".next/**", "!.next/cache/**", ".cache/.tsbuildinfo"], + "dependsOn": ["cir-dep", "format", "lint", "^build"] }, "lint": { "cache": true, @@ -18,6 +20,7 @@ "outputLogs": "errors-only" }, "dev": { + "outputs": ["dist/**"], "cache": false, "persistent": true, "dependsOn": ["cir-dep", "format"] diff --git a/yarn.lock b/yarn.lock index 12dcfba..45b928c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -223,26 +223,26 @@ __metadata: languageName: node linkType: hard -"@emotion/babel-plugin@npm:^11.12.0": - version: 11.12.0 - resolution: "@emotion/babel-plugin@npm:11.12.0" +"@emotion/babel-plugin@npm:^11.13.5": + version: 11.13.5 + resolution: "@emotion/babel-plugin@npm:11.13.5" dependencies: "@babel/helper-module-imports": "npm:^7.16.7" "@babel/runtime": "npm:^7.18.3" "@emotion/hash": "npm:^0.9.2" "@emotion/memoize": "npm:^0.9.0" - "@emotion/serialize": "npm:^1.2.0" + "@emotion/serialize": "npm:^1.3.3" babel-plugin-macros: "npm:^3.1.0" convert-source-map: "npm:^1.5.0" escape-string-regexp: "npm:^4.0.0" find-root: "npm:^1.1.0" source-map: "npm:^0.5.7" stylis: "npm:4.2.0" - checksum: 10c0/930ff6f8768b0c24d05896ad696be20e1c65f32ed61fb5c1488f571120a947ef0a2cf69187b17114cc76e7886f771fac150876ed7b5341324fec2377185d6573 + checksum: 10c0/8ccbfec7defd0e513cb8a1568fa179eac1e20c35fda18aed767f6c59ea7314363ebf2de3e9d2df66c8ad78928dc3dceeded84e6fa8059087cae5c280090aeeeb languageName: node linkType: hard -"@emotion/cache@npm:^11.13.0, @emotion/cache@npm:^11.13.1": +"@emotion/cache@npm:^11.13.1": version: 11.13.1 resolution: "@emotion/cache@npm:11.13.1" dependencies: @@ -255,6 +255,19 @@ __metadata: languageName: node linkType: hard +"@emotion/cache@npm:^11.13.5": + version: 11.13.5 + resolution: "@emotion/cache@npm:11.13.5" + dependencies: + "@emotion/memoize": "npm:^0.9.0" + "@emotion/sheet": "npm:^1.4.0" + "@emotion/utils": "npm:^1.4.2" + "@emotion/weak-memoize": "npm:^0.4.0" + stylis: "npm:4.2.0" + checksum: 10c0/fc669bf2add27ddff7b1f341b54e7124379156285095f0b38fb846efe90c64c70d2826f73bc734358a4fce04578229774a38ff4de2599d286461bfca57ba7d23 + languageName: node + linkType: hard + "@emotion/hash@npm:^0.9.2": version: 0.9.2 resolution: "@emotion/hash@npm:0.9.2" @@ -278,16 +291,16 @@ __metadata: languageName: node linkType: hard -"@emotion/react@npm:11.13.3": - version: 11.13.3 - resolution: "@emotion/react@npm:11.13.3" +"@emotion/react@npm:11.13.5": + version: 11.13.5 + resolution: "@emotion/react@npm:11.13.5" dependencies: "@babel/runtime": "npm:^7.18.3" - "@emotion/babel-plugin": "npm:^11.12.0" - "@emotion/cache": "npm:^11.13.0" - "@emotion/serialize": "npm:^1.3.1" + "@emotion/babel-plugin": "npm:^11.13.5" + "@emotion/cache": "npm:^11.13.5" + "@emotion/serialize": "npm:^1.3.3" "@emotion/use-insertion-effect-with-fallbacks": "npm:^1.1.0" - "@emotion/utils": "npm:^1.4.0" + "@emotion/utils": "npm:^1.4.2" "@emotion/weak-memoize": "npm:^0.4.0" hoist-non-react-statics: "npm:^3.3.1" peerDependencies: @@ -295,11 +308,11 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10c0/a55e770b9ea35de5d35db05a7ad40a4a3f442809fa8e4fabaf56da63ac9444f09aaf691c4e75a1455dc388991ab0c0ab4e253ce67c5836f27513e45ebd01b673 + checksum: 10c0/16b4810bc68c619cb25145e543880e905fc99332bacc1c39b20c913b2e6130289d9acd909abba55820fa796c5cca3cade6fe79a26b3ab7e4e2d040c61ee14a6e languageName: node linkType: hard -"@emotion/serialize@npm:^1.2.0, @emotion/serialize@npm:^1.3.0, @emotion/serialize@npm:^1.3.1, @emotion/serialize@npm:^1.3.2": +"@emotion/serialize@npm:^1.3.2": version: 1.3.2 resolution: "@emotion/serialize@npm:1.3.2" dependencies: @@ -312,6 +325,19 @@ __metadata: languageName: node linkType: hard +"@emotion/serialize@npm:^1.3.3": + version: 1.3.3 + resolution: "@emotion/serialize@npm:1.3.3" + dependencies: + "@emotion/hash": "npm:^0.9.2" + "@emotion/memoize": "npm:^0.9.0" + "@emotion/unitless": "npm:^0.10.0" + "@emotion/utils": "npm:^1.4.2" + csstype: "npm:^3.0.2" + checksum: 10c0/b28cb7de59de382021de2b26c0c94ebbfb16967a1b969a56fdb6408465a8993df243bfbd66430badaa6800e1834724e84895f5a6a9d97d0d224de3d77852acb4 + languageName: node + linkType: hard + "@emotion/server@npm:11.11.0": version: 11.11.0 resolution: "@emotion/server@npm:11.11.0" @@ -336,23 +362,23 @@ __metadata: languageName: node linkType: hard -"@emotion/styled@npm:11.13.0": - version: 11.13.0 - resolution: "@emotion/styled@npm:11.13.0" +"@emotion/styled@npm:11.13.5": + version: 11.13.5 + resolution: "@emotion/styled@npm:11.13.5" dependencies: "@babel/runtime": "npm:^7.18.3" - "@emotion/babel-plugin": "npm:^11.12.0" + "@emotion/babel-plugin": "npm:^11.13.5" "@emotion/is-prop-valid": "npm:^1.3.0" - "@emotion/serialize": "npm:^1.3.0" + "@emotion/serialize": "npm:^1.3.3" "@emotion/use-insertion-effect-with-fallbacks": "npm:^1.1.0" - "@emotion/utils": "npm:^1.4.0" + "@emotion/utils": "npm:^1.4.2" peerDependencies: "@emotion/react": ^11.0.0-rc.0 react: ">=16.8.0" peerDependenciesMeta: "@types/react": optional: true - checksum: 10c0/5e2cc85c8a2f6e7bd012731cf0b6da3aef5906225e87e8d4a5c19da50572e24d9aaf92615aa36aa863f0fe6b62a121033356e1cad62617c48bfdaa2c3cf0d8a4 + checksum: 10c0/18d3e38482f92c93446fbfe46e3ca2b182f228f3317ca23f9bd69ddc313bacabf8ecf4d7e720e9aa492bd651cb0b8f87196547bd136666ef50287c414cd36936 languageName: node linkType: hard @@ -379,6 +405,13 @@ __metadata: languageName: node linkType: hard +"@emotion/utils@npm:^1.4.2": + version: 1.4.2 + resolution: "@emotion/utils@npm:1.4.2" + checksum: 10c0/7d0010bf60a2a8c1a033b6431469de4c80e47aeb8fd856a17c1d1f76bbc3a03161a34aeaa78803566e29681ca551e7bf9994b68e9c5f5c796159923e44f78d9a + languageName: node + linkType: hard + "@emotion/weak-memoize@npm:^0.4.0": version: 0.4.0 resolution: "@emotion/weak-memoize@npm:0.4.0" @@ -404,39 +437,39 @@ __metadata: languageName: node linkType: hard -"@eslint/compat@npm:1.2.2": - version: 1.2.2 - resolution: "@eslint/compat@npm:1.2.2" +"@eslint/compat@npm:1.2.3": + version: 1.2.3 + resolution: "@eslint/compat@npm:1.2.3" peerDependencies: eslint: ^9.10.0 peerDependenciesMeta: eslint: optional: true - checksum: 10c0/c19e1765673520daf6f08bb82f957c6b42079389725ceda99a4387c403fccd5f9a99d142feec43ed032cb240038ea67db9748b17bf8de4ceb8b2fba382089780 + checksum: 10c0/b7439e62f73b9a05abea3b54ad8edc171e299171fc4673fc5a2c84d97a584bb9487a7f0bee397342f6574bd53597819a8abe52f1ca72184378cf387275b84e32 languageName: node linkType: hard -"@eslint/config-array@npm:^0.18.0": - version: 0.18.0 - resolution: "@eslint/config-array@npm:0.18.0" +"@eslint/config-array@npm:^0.19.0": + version: 0.19.0 + resolution: "@eslint/config-array@npm:0.19.0" dependencies: "@eslint/object-schema": "npm:^2.1.4" debug: "npm:^4.3.1" minimatch: "npm:^3.1.2" - checksum: 10c0/0234aeb3e6b052ad2402a647d0b4f8a6aa71524bafe1adad0b8db1dfe94d7f5f26d67c80f79bb37ac61361a1d4b14bb8fb475efe501de37263cf55eabb79868f + checksum: 10c0/def23c6c67a8f98dc88f1b87e17a5668e5028f5ab9459661aabfe08e08f2acd557474bbaf9ba227be0921ae4db232c62773dbb7739815f8415678eb8f592dbf5 languageName: node linkType: hard -"@eslint/core@npm:^0.7.0": - version: 0.7.0 - resolution: "@eslint/core@npm:0.7.0" - checksum: 10c0/3cdee8bc6cbb96ac6103d3ead42e59830019435839583c9eb352b94ed558bd78e7ffad5286dc710df21ec1e7bd8f52aa6574c62457a4dd0f01f3736fa4a7d87a +"@eslint/core@npm:^0.9.0": + version: 0.9.0 + resolution: "@eslint/core@npm:0.9.0" + checksum: 10c0/6d8e8e0991cef12314c49425d8d2d9394f5fb1a36753ff82df7c03185a4646cb7c8736cf26638a4a714782cedf4b23cfc17667d282d3e5965b3920a0e7ce20d4 languageName: node linkType: hard -"@eslint/eslintrc@npm:3.1.0, @eslint/eslintrc@npm:^3.1.0": - version: 3.1.0 - resolution: "@eslint/eslintrc@npm:3.1.0" +"@eslint/eslintrc@npm:3.2.0, @eslint/eslintrc@npm:^3.2.0": + version: 3.2.0 + resolution: "@eslint/eslintrc@npm:3.2.0" dependencies: ajv: "npm:^6.12.4" debug: "npm:^4.3.2" @@ -447,14 +480,14 @@ __metadata: js-yaml: "npm:^4.1.0" minimatch: "npm:^3.1.2" strip-json-comments: "npm:^3.1.1" - checksum: 10c0/5b7332ed781edcfc98caa8dedbbb843abfb9bda2e86538529c843473f580e40c69eb894410eddc6702f487e9ee8f8cfa8df83213d43a8fdb549f23ce06699167 + checksum: 10c0/43867a07ff9884d895d9855edba41acf325ef7664a8df41d957135a81a477ff4df4196f5f74dc3382627e5cc8b7ad6b815c2cea1b58f04a75aced7c43414ab8b languageName: node linkType: hard -"@eslint/js@npm:9.14.0": - version: 9.14.0 - resolution: "@eslint/js@npm:9.14.0" - checksum: 10c0/a423dd435e10aa3b461599aa02f6cbadd4b5128cb122467ee4e2c798e7ca4f9bb1fce4dcea003b29b983090238cf120899c1af657cf86300b399e4f996b83ddc +"@eslint/js@npm:9.15.0": + version: 9.15.0 + resolution: "@eslint/js@npm:9.15.0" + checksum: 10c0/56552966ab1aa95332f70d0e006db5746b511c5f8b5e0c6a9b2d6764ff6d964e0b2622731877cbc4e3f0e74c5b39191290d5f48147be19175292575130d499ab languageName: node linkType: hard @@ -465,12 +498,12 @@ __metadata: languageName: node linkType: hard -"@eslint/plugin-kit@npm:^0.2.0": - version: 0.2.2 - resolution: "@eslint/plugin-kit@npm:0.2.2" +"@eslint/plugin-kit@npm:^0.2.3": + version: 0.2.3 + resolution: "@eslint/plugin-kit@npm:0.2.3" dependencies: levn: "npm:^0.4.1" - checksum: 10c0/ec533ccc99f2ab003d6f64495cff853730fb7d8bc0eaf031ffccc68de7c34c74a2eda50dfa759cacfb409f014c98d306714c995348d5383c9d3140f9f80a5895 + checksum: 10c0/89a8035976bb1780e3fa8ffe682df013bd25f7d102d991cecd3b7c297f4ce8c1a1b6805e76dd16465b5353455b670b545eff2b4ec3133e0eab81a5f9e99bd90f languageName: node linkType: hard @@ -481,159 +514,159 @@ __metadata: languageName: node linkType: hard -"@firebase/analytics-compat@npm:0.2.15": - version: 0.2.15 - resolution: "@firebase/analytics-compat@npm:0.2.15" +"@firebase/analytics-compat@npm:0.2.16": + version: 0.2.16 + resolution: "@firebase/analytics-compat@npm:0.2.16" dependencies: - "@firebase/analytics": "npm:0.10.9" - "@firebase/analytics-types": "npm:0.8.2" - "@firebase/component": "npm:0.6.10" - "@firebase/util": "npm:1.10.1" + "@firebase/analytics": "npm:0.10.10" + "@firebase/analytics-types": "npm:0.8.3" + "@firebase/component": "npm:0.6.11" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app-compat": 0.x - checksum: 10c0/03447371ef63efed1f3f7de2687c3103c16b28ac61e217effe63ec0f4c9de6743f0a991f1e5557cb2b24da56f07bd62d71a6a9164db9073f8d053ceec976d5ba + checksum: 10c0/c4a91732827cb16c91bb2f19a77d85c274a0a1de20fd2ae2fcb92a55d6fe5cd60fe66ef687f80467f75aeaa49c3a2c68d485616b98a5f5c7f3a3f7960eb9b2a6 languageName: node linkType: hard -"@firebase/analytics-types@npm:0.8.2": - version: 0.8.2 - resolution: "@firebase/analytics-types@npm:0.8.2" - checksum: 10c0/0345beed0e36637c3e3f5c0638478fbd0d165d197a0374dd848c4bb772298b1eb3f3bccfea1f4501e32ee9a4ae8ac1c30bf399645f60037b2b08f4b5e252ec78 +"@firebase/analytics-types@npm:0.8.3": + version: 0.8.3 + resolution: "@firebase/analytics-types@npm:0.8.3" + checksum: 10c0/2cbc5fe8425bc01c7ba03579cdc5ca6b23de51b08edb62927be610a33bbc961bae97aa48ee12dcdb039b752c158d095f234ed20f1f4d2bd7a5c39f44d82cdf22 languageName: node linkType: hard -"@firebase/analytics@npm:0.10.9": - version: 0.10.9 - resolution: "@firebase/analytics@npm:0.10.9" +"@firebase/analytics@npm:0.10.10": + version: 0.10.10 + resolution: "@firebase/analytics@npm:0.10.10" dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/installations": "npm:0.6.10" - "@firebase/logger": "npm:0.4.3" - "@firebase/util": "npm:1.10.1" + "@firebase/component": "npm:0.6.11" + "@firebase/installations": "npm:0.6.11" + "@firebase/logger": "npm:0.4.4" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app": 0.x - checksum: 10c0/154d9d8b6cf7fc7062314d8431cb7029518c6932681ecd27e4d3e8440b3596f356ca8020efc41c3cbccef57dd8c66c60a04d8628b98edc1f3868d3d3c90c9980 + checksum: 10c0/909f191e1ff8046088387a6fca901834fb0378b4e75314d27a605011559a9d06cd0bbb04826e552907ecce459d158c56c982e032b5383f1dabe8d8c906ce9f01 languageName: node linkType: hard -"@firebase/app-check-compat@npm:0.3.16": - version: 0.3.16 - resolution: "@firebase/app-check-compat@npm:0.3.16" +"@firebase/app-check-compat@npm:0.3.17": + version: 0.3.17 + resolution: "@firebase/app-check-compat@npm:0.3.17" dependencies: - "@firebase/app-check": "npm:0.8.9" - "@firebase/app-check-types": "npm:0.5.2" - "@firebase/component": "npm:0.6.10" - "@firebase/logger": "npm:0.4.3" - "@firebase/util": "npm:1.10.1" + "@firebase/app-check": "npm:0.8.10" + "@firebase/app-check-types": "npm:0.5.3" + "@firebase/component": "npm:0.6.11" + "@firebase/logger": "npm:0.4.4" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app-compat": 0.x - checksum: 10c0/ef861a521ada31f28ae7aa245441a38786cd23103077f57658e553ab53cbb190acacb87be107ef082ae9122352f48b9a4615bc982690eaa93a5ae5b0c1124097 + checksum: 10c0/3e89e78d044c66c1d036f6f078eb27e6738c0a017cd1e71edb9e3b40efdd61fe36a2c6c4de755ff0cef15574f70c178c8874f95f709ace68013934f6c160a235 languageName: node linkType: hard -"@firebase/app-check-interop-types@npm:0.3.2": - version: 0.3.2 - resolution: "@firebase/app-check-interop-types@npm:0.3.2" - checksum: 10c0/7f1d25bc6cef3e4a209e6db096f6088b132b80f59947026af269406bdfbf140f391aeb94e68ecb4f524b4382b7217cc500cc068eeaf834e9665b7793177cc3f8 +"@firebase/app-check-interop-types@npm:0.3.3": + version: 0.3.3 + resolution: "@firebase/app-check-interop-types@npm:0.3.3" + checksum: 10c0/4a887ef5e30ee1a407b569603c433a9f21244d50a19d97a5f1f17d8f5caea83096852b39e67d599f3238f1f7e2a369b02d184a184986a649ed1f8fed12fbd6be languageName: node linkType: hard -"@firebase/app-check-types@npm:0.5.2": - version: 0.5.2 - resolution: "@firebase/app-check-types@npm:0.5.2" - checksum: 10c0/0e1e3c89da6591c608647faefd49add3aed8a3d5af061c6f4d192fa52cd48a9c511df3dfda96eac5cf18fde2661361bb26a18c9c346b300f71ffa743a85aeb68 +"@firebase/app-check-types@npm:0.5.3": + version: 0.5.3 + resolution: "@firebase/app-check-types@npm:0.5.3" + checksum: 10c0/59af0ae698ff2172e84f504e3b5e778c2cc78fefdcceb917eb899a204ad130ad5497011ab94459f6f9dd0a9062a0455bbd745ad3e488b39dae4625c3fb0d0145 languageName: node linkType: hard -"@firebase/app-check@npm:0.8.9": - version: 0.8.9 - resolution: "@firebase/app-check@npm:0.8.9" +"@firebase/app-check@npm:0.8.10": + version: 0.8.10 + resolution: "@firebase/app-check@npm:0.8.10" dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/logger": "npm:0.4.3" - "@firebase/util": "npm:1.10.1" + "@firebase/component": "npm:0.6.11" + "@firebase/logger": "npm:0.4.4" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app": 0.x - checksum: 10c0/80215ef760370b162bdd0853cad076b1b7b2903b7a53cbe33706e0d52743cf3d87abe6369ba0e13ce506659525ca1e576e05ae4192f1611d4de0f9dd3c52cd0e + checksum: 10c0/0e189362413b7592f13dcd0dc471cad2d94b3927272d6b0e839c7020c3427ae22d92f57246b49e88d2d952c6651cb1bd4c1c7fb0b9b5134eb7928dcc3aa02468 languageName: node linkType: hard -"@firebase/app-compat@npm:0.2.45": - version: 0.2.45 - resolution: "@firebase/app-compat@npm:0.2.45" +"@firebase/app-compat@npm:0.2.46": + version: 0.2.46 + resolution: "@firebase/app-compat@npm:0.2.46" dependencies: - "@firebase/app": "npm:0.10.15" - "@firebase/component": "npm:0.6.10" - "@firebase/logger": "npm:0.4.3" - "@firebase/util": "npm:1.10.1" + "@firebase/app": "npm:0.10.16" + "@firebase/component": "npm:0.6.11" + "@firebase/logger": "npm:0.4.4" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" - checksum: 10c0/f5a5a56d31118685c9bbbb4c38546b3c29c418c30ecba5c597274eba75f8145708478027ca65b9538e5af94ca5db0996c9c8ca9046e3157c15c94014b56fd254 + checksum: 10c0/20033c024ecf1650a4f9018ac7f614a91f836a05e77eb4f692225e25c68f982c38634c4bff2c417500980678f14afde426fa766b8eb79f2021807f6909cd792d languageName: node linkType: hard -"@firebase/app-types@npm:0.9.2": - version: 0.9.2 - resolution: "@firebase/app-types@npm:0.9.2" - checksum: 10c0/6bc78395ecadbf4958f1300ce9eb1d80522f05531acbacd88220fb77f4b924355bc920afe7f09c29acc40f374380e36539647604e1dab2fea045622b24988441 +"@firebase/app-types@npm:0.9.3": + version: 0.9.3 + resolution: "@firebase/app-types@npm:0.9.3" + checksum: 10c0/02ec9a26c10b9bbb2a1e5b9ae0552b5325b40066e3c23be089ceae53414a1505f2ab716ae1098652a0a0c9992ba322c05371a9b2a837cccfae309788372a72e0 languageName: node linkType: hard -"@firebase/app@npm:0.10.15": - version: 0.10.15 - resolution: "@firebase/app@npm:0.10.15" +"@firebase/app@npm:0.10.16": + version: 0.10.16 + resolution: "@firebase/app@npm:0.10.16" dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/logger": "npm:0.4.3" - "@firebase/util": "npm:1.10.1" + "@firebase/component": "npm:0.6.11" + "@firebase/logger": "npm:0.4.4" + "@firebase/util": "npm:1.10.2" idb: "npm:7.1.1" tslib: "npm:^2.1.0" - checksum: 10c0/b5bc80a82e29723361b194a56d405d9573f84cecb23df6c6b341e3ba27fc4ff7c641c08a4bcb4a3a349d566e4948bca403f8c79575f2d50944ecba34035acb32 + checksum: 10c0/a56d0ece4bc22ff7630561388afc9800a4ba6bf3f0f139d5fe41744e67e51c7c6ac727ddaf1d30f5b05aef0e693c4d717a901d05e9ba47e72dbdd1bbf4bc4503 languageName: node linkType: hard -"@firebase/auth-compat@npm:0.5.15": - version: 0.5.15 - resolution: "@firebase/auth-compat@npm:0.5.15" +"@firebase/auth-compat@npm:0.5.16": + version: 0.5.16 + resolution: "@firebase/auth-compat@npm:0.5.16" dependencies: - "@firebase/auth": "npm:1.8.0" - "@firebase/auth-types": "npm:0.12.2" - "@firebase/component": "npm:0.6.10" - "@firebase/util": "npm:1.10.1" + "@firebase/auth": "npm:1.8.1" + "@firebase/auth-types": "npm:0.12.3" + "@firebase/component": "npm:0.6.11" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app-compat": 0.x - checksum: 10c0/bcc6e2b3905f1244615cfbcfcad1f4252066a0b933735eeb9e4111d803a68b96b7c29de2c8639d5a07ac41cc60612d2a14905771748335c2408ea2d857bf6a2e + checksum: 10c0/01ebf2251c5d995f2b7d87523875da10a813f76af4d13fef2fb4be3dcd407252e65f8c48af9c9b2d8cc0cdda8294de27674489e21b431f8b82a8b2c38d82067b languageName: node linkType: hard -"@firebase/auth-interop-types@npm:0.2.3": - version: 0.2.3 - resolution: "@firebase/auth-interop-types@npm:0.2.3" - checksum: 10c0/a3e72134a5ba177c87e2a35064f88ec6e9272f582c0754664edaabf23e2dcc1e8f9b70f78521c128d20c8ed060e857f333a9c6d5b463e6612bddef01b070da06 +"@firebase/auth-interop-types@npm:0.2.4": + version: 0.2.4 + resolution: "@firebase/auth-interop-types@npm:0.2.4" + checksum: 10c0/ff833bcbb472992c6061847309e338dac736c616522c5fd808526d6dc13b9788458a8c9677d91c33c1288ee38f42896c2b4b8fe10ee74f1569d11f3f3c4f53b5 languageName: node linkType: hard -"@firebase/auth-types@npm:0.12.2": - version: 0.12.2 - resolution: "@firebase/auth-types@npm:0.12.2" +"@firebase/auth-types@npm:0.12.3": + version: 0.12.3 + resolution: "@firebase/auth-types@npm:0.12.3" peerDependencies: "@firebase/app-types": 0.x "@firebase/util": 1.x - checksum: 10c0/daf3d785cf7c3bb0fde7a92781f11419f7543980e28ad24eebba61ee448ca9858cdd7cbab91d9c4dcc0b7c21708b72dca45fef49f45af715f7ddfe8d545fafbd + checksum: 10c0/8666c6b7dda15965ad0300c18c742eb10e5f3a49fa255e169fd8af2b5b2088e65db24f66eaa7889ef92626c6a3de0b7f1a05960c4e9645f4d1111601121cb148 languageName: node linkType: hard -"@firebase/auth@npm:1.8.0": - version: 1.8.0 - resolution: "@firebase/auth@npm:1.8.0" +"@firebase/auth@npm:1.8.1": + version: 1.8.1 + resolution: "@firebase/auth@npm:1.8.1" dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/logger": "npm:0.4.3" - "@firebase/util": "npm:1.10.1" + "@firebase/component": "npm:0.6.11" + "@firebase/logger": "npm:0.4.4" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app": 0.x @@ -641,448 +674,381 @@ __metadata: peerDependenciesMeta: "@react-native-async-storage/async-storage": optional: true - checksum: 10c0/e768a93cfb134c1df4511a4d6f63aef09eb18a7aaf6459cdd3554770e4be8ad787dab104bf9be275ca1f2326f7bdf57abb2e894666ed2aebeab0fb7f68d49d82 - languageName: node - linkType: hard - -"@firebase/component@npm:0.6.10": - version: 0.6.10 - resolution: "@firebase/component@npm:0.6.10" - dependencies: - "@firebase/util": "npm:1.10.1" - tslib: "npm:^2.1.0" - checksum: 10c0/cbcb9c575d0eccccc161c1c02d87d23e184f8d2bf7b45f0cd966f7f4668816d0a90a29795b379cbaebbe278f98d2cc94c6f405bc4c4694ab37beaa333c85fb39 + checksum: 10c0/01755c08fda1fea7b50ba22a5cb0e3663be62c9096d0d48201e54ad5d96c4a24259b45117163a150a20cecdf606133b14b939cb5219c28b0c4bd4f003db978e4 languageName: node linkType: hard -"@firebase/component@npm:0.6.9": - version: 0.6.9 - resolution: "@firebase/component@npm:0.6.9" +"@firebase/component@npm:0.6.11": + version: 0.6.11 + resolution: "@firebase/component@npm:0.6.11" dependencies: - "@firebase/util": "npm:1.10.0" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" - checksum: 10c0/609dd193000dd9bdd12d820fbf2653d693e9aa2f768aa7817573e4f349b83ae4aa3b80ccd13b5cde4fb6bdf924a283a33ba0b608896bf6112db9265607202d28 + checksum: 10c0/788d66a0acb506507042173d1906edaf533ca68405f84aed16f33d8f2a130a8796e2f5c2d80177fc6c1826b74ea510da4541df9c381f6bf0f2b5417d3527797c languageName: node linkType: hard -"@firebase/data-connect@npm:0.1.1": - version: 0.1.1 - resolution: "@firebase/data-connect@npm:0.1.1" +"@firebase/data-connect@npm:0.1.2": + version: 0.1.2 + resolution: "@firebase/data-connect@npm:0.1.2" dependencies: - "@firebase/auth-interop-types": "npm:0.2.3" - "@firebase/component": "npm:0.6.10" - "@firebase/logger": "npm:0.4.3" - "@firebase/util": "npm:1.10.1" + "@firebase/auth-interop-types": "npm:0.2.4" + "@firebase/component": "npm:0.6.11" + "@firebase/logger": "npm:0.4.4" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app": 0.x - checksum: 10c0/043003fcced645541e12e1c929a854a41d4cb8186595e2c9b0db9e8a5fba1d14547075c6faa4da01dbe77221f3ebbad4a9f03d81f12e006b8912a43a9afaa842 - languageName: node - linkType: hard - -"@firebase/database-compat@npm:1.0.8": - version: 1.0.8 - resolution: "@firebase/database-compat@npm:1.0.8" - dependencies: - "@firebase/component": "npm:0.6.9" - "@firebase/database": "npm:1.0.8" - "@firebase/database-types": "npm:1.0.5" - "@firebase/logger": "npm:0.4.2" - "@firebase/util": "npm:1.10.0" - tslib: "npm:^2.1.0" - checksum: 10c0/34456da205dc0376601cef43ac1eb22b9bddac0555ccde14d759e0737b041bad6b996335f824543e4d782e9440893ae9c09e28be2c26c6afc6dbbfedd2c3eb84 + checksum: 10c0/b66de47e63251c4239c6d123dedc6043f0988802f899b7c2efcf33c6dc4f349e55fb4e54de43e581083589105fb2844d3f720e84efb3d82c13907a1bddc980c4 languageName: node linkType: hard -"@firebase/database-compat@npm:2.0.0": - version: 2.0.0 - resolution: "@firebase/database-compat@npm:2.0.0" +"@firebase/database-compat@npm:2.0.1, @firebase/database-compat@npm:^2.0.0": + version: 2.0.1 + resolution: "@firebase/database-compat@npm:2.0.1" dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/database": "npm:1.0.9" - "@firebase/database-types": "npm:1.0.6" - "@firebase/logger": "npm:0.4.3" - "@firebase/util": "npm:1.10.1" + "@firebase/component": "npm:0.6.11" + "@firebase/database": "npm:1.0.10" + "@firebase/database-types": "npm:1.0.7" + "@firebase/logger": "npm:0.4.4" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" - checksum: 10c0/dd1ffeab13f46b9f2218e56573c77d211757e8aa3650494f1e9d51d875cac2b6f5f0eb800f2d8f48ccdebe9ce5ea8d81a77ae87836de2d1344599534aa4811a4 + checksum: 10c0/e63c3f432d49c0cebc7f36da97d497ece86fa7d1d68bc59020395f96a3e10a16acf299d6299127a4ef8b8abd5f08ea257c5de3e9af44640f4517021a21495a4f languageName: node linkType: hard -"@firebase/database-types@npm:1.0.5": - version: 1.0.5 - resolution: "@firebase/database-types@npm:1.0.5" - dependencies: - "@firebase/app-types": "npm:0.9.2" - "@firebase/util": "npm:1.10.0" - checksum: 10c0/64067fd5f11117898ec499bd63b04e13e0a3ef08c82d10873c112ef86be503152d0848f996d6f3f178392a141f20206d7cadb8e3163fd7ffaf7221c132d0f7a2 - languageName: node - linkType: hard - -"@firebase/database-types@npm:1.0.6": - version: 1.0.6 - resolution: "@firebase/database-types@npm:1.0.6" +"@firebase/database-types@npm:1.0.7, @firebase/database-types@npm:^1.0.6": + version: 1.0.7 + resolution: "@firebase/database-types@npm:1.0.7" dependencies: - "@firebase/app-types": "npm:0.9.2" - "@firebase/util": "npm:1.10.1" - checksum: 10c0/a7f11f9947e9653dd9912d86b99025f01a9f9e80cd0f0ad02fc0fa4848baeba7f1041865e4516575ddc573d64d750b455db1325edd56db6c8fb03fa4b85d7919 + "@firebase/app-types": "npm:0.9.3" + "@firebase/util": "npm:1.10.2" + checksum: 10c0/12c1f6b489d662f1191b65c1cd08cea1c60591f24867241d8861cf5c21e0b6402f7af2e832e35bc43cdc94dd00658da0d124009d4b3ab036f188299fbb8561d8 languageName: node linkType: hard -"@firebase/database@npm:1.0.8": - version: 1.0.8 - resolution: "@firebase/database@npm:1.0.8" - dependencies: - "@firebase/app-check-interop-types": "npm:0.3.2" - "@firebase/auth-interop-types": "npm:0.2.3" - "@firebase/component": "npm:0.6.9" - "@firebase/logger": "npm:0.4.2" - "@firebase/util": "npm:1.10.0" - faye-websocket: "npm:0.11.4" - tslib: "npm:^2.1.0" - checksum: 10c0/dac0f0d1836cdd1ccc4785bdf35a1cc35a00d35c5c3d21dd87afccd1873f10ed56a606c72de07dbc93600115cd5a94686fbcf169e34ee9ae19a184469c110810 - languageName: node - linkType: hard - -"@firebase/database@npm:1.0.9": - version: 1.0.9 - resolution: "@firebase/database@npm:1.0.9" +"@firebase/database@npm:1.0.10": + version: 1.0.10 + resolution: "@firebase/database@npm:1.0.10" dependencies: - "@firebase/app-check-interop-types": "npm:0.3.2" - "@firebase/auth-interop-types": "npm:0.2.3" - "@firebase/component": "npm:0.6.10" - "@firebase/logger": "npm:0.4.3" - "@firebase/util": "npm:1.10.1" + "@firebase/app-check-interop-types": "npm:0.3.3" + "@firebase/auth-interop-types": "npm:0.2.4" + "@firebase/component": "npm:0.6.11" + "@firebase/logger": "npm:0.4.4" + "@firebase/util": "npm:1.10.2" faye-websocket: "npm:0.11.4" tslib: "npm:^2.1.0" - checksum: 10c0/1d448540dc47f28127a0ad72daf112072437486b7b5ce07967d1a8a683137c8877b3b377e047cb2cc0392d7a72d874528b8cb058c339cc7cc1ea31899f3370d9 + checksum: 10c0/c159b14f91824ce37c59630ac8333befb63e223289a0fbed4f8a6551a39090dc9893a5a34b89034888d18fa80a1831567688273a07d08f4a101bb206a02daf9a languageName: node linkType: hard -"@firebase/firestore-compat@npm:0.3.39": - version: 0.3.39 - resolution: "@firebase/firestore-compat@npm:0.3.39" +"@firebase/firestore-compat@npm:0.3.40": + version: 0.3.40 + resolution: "@firebase/firestore-compat@npm:0.3.40" dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/firestore": "npm:4.7.4" - "@firebase/firestore-types": "npm:3.0.2" - "@firebase/util": "npm:1.10.1" + "@firebase/component": "npm:0.6.11" + "@firebase/firestore": "npm:4.7.5" + "@firebase/firestore-types": "npm:3.0.3" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app-compat": 0.x - checksum: 10c0/ac5671ba0b3b8ace41d6ed4c7b5291551d53b52f534eba7a799d014eee12d842e5545791d6dff5d251e208bd362645968e852cc4d5e9cfb96538bd93817987bd + checksum: 10c0/3cdaf8789e860d7460d36265516bb5516d252307c0cb58b00272c51f856973c347c13e119c258c5b2eb39071409c16fa84f583b8396d39248668cec0a164991d languageName: node linkType: hard -"@firebase/firestore-types@npm:3.0.2": - version: 3.0.2 - resolution: "@firebase/firestore-types@npm:3.0.2" +"@firebase/firestore-types@npm:3.0.3": + version: 3.0.3 + resolution: "@firebase/firestore-types@npm:3.0.3" peerDependencies: "@firebase/app-types": 0.x "@firebase/util": 1.x - checksum: 10c0/3f8d97894d6bbef7a15ec5a33b241ddbb6ee90c3316c13f2a38fe5b8333e6b842197b498ec7d597ecd52ba4d5253ee96fcc6c889e9b394156200950577bbbded + checksum: 10c0/8196168a2de68bd60e0a9053a670d14d2917bf8e30829a4a2f8435fa2aceaaf97ce7438cd9525786a9bf8c5d6104ced3086acd792439371fea7b35497a53bdfa languageName: node linkType: hard -"@firebase/firestore@npm:4.7.4": - version: 4.7.4 - resolution: "@firebase/firestore@npm:4.7.4" +"@firebase/firestore@npm:4.7.5": + version: 4.7.5 + resolution: "@firebase/firestore@npm:4.7.5" dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/logger": "npm:0.4.3" - "@firebase/util": "npm:1.10.1" - "@firebase/webchannel-wrapper": "npm:1.0.2" + "@firebase/component": "npm:0.6.11" + "@firebase/logger": "npm:0.4.4" + "@firebase/util": "npm:1.10.2" + "@firebase/webchannel-wrapper": "npm:1.0.3" "@grpc/grpc-js": "npm:~1.9.0" "@grpc/proto-loader": "npm:^0.7.8" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app": 0.x - checksum: 10c0/6d1e64385e497288f0e0f78fc29667708c640301fc6d370701ab18887c6a4cc1054bc933054c1eb695aebf91a007bd968e2b8a974bfadd74fac5f5625b43a33b + checksum: 10c0/e176609c492b39231514f31c1f68c42cc7093a781d05124c68cf6aaf29d82bf9129a0ac6d02325d5408d34a092368128bd7e073fe490f93c72c9f6c3bf4851aa languageName: node linkType: hard -"@firebase/functions-compat@npm:0.3.15": - version: 0.3.15 - resolution: "@firebase/functions-compat@npm:0.3.15" +"@firebase/functions-compat@npm:0.3.16": + version: 0.3.16 + resolution: "@firebase/functions-compat@npm:0.3.16" dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/functions": "npm:0.11.9" - "@firebase/functions-types": "npm:0.6.2" - "@firebase/util": "npm:1.10.1" + "@firebase/component": "npm:0.6.11" + "@firebase/functions": "npm:0.11.10" + "@firebase/functions-types": "npm:0.6.3" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app-compat": 0.x - checksum: 10c0/1db82b79f17f7cefa805ac2a603229d7092812df99c3035e7ab187775951b485efb4b9603c1a87f9901cb138d756a794530b72528ec4a4ca4448705bda17a75a + checksum: 10c0/840e579344734004234c0ddf6a42f4a094d8a958e7dc6555aac7e172b427a2df9fa0a77a765a6c456a3afe33af9918cf87bbb61cfdc771d04b4ebc8802353a9e languageName: node linkType: hard -"@firebase/functions-types@npm:0.6.2": - version: 0.6.2 - resolution: "@firebase/functions-types@npm:0.6.2" - checksum: 10c0/36ea0b30f4cd8d28fc574870780439642048d25bbed289f37f2567f7d93bac80dc19d03e5e7131e879f1f354f6ad7f6cf70188edaf6dbe005b98403e50224054 +"@firebase/functions-types@npm:0.6.3": + version: 0.6.3 + resolution: "@firebase/functions-types@npm:0.6.3" + checksum: 10c0/aabd7bdd8c479323a419bba9ad275d96cd44229bd2213c87be08a9978af5ff0c1306279229a358c77280ce54fa6f42c91a6fd6c947808b1103174db0261b86e1 languageName: node linkType: hard -"@firebase/functions@npm:0.11.9": - version: 0.11.9 - resolution: "@firebase/functions@npm:0.11.9" +"@firebase/functions@npm:0.11.10": + version: 0.11.10 + resolution: "@firebase/functions@npm:0.11.10" dependencies: - "@firebase/app-check-interop-types": "npm:0.3.2" - "@firebase/auth-interop-types": "npm:0.2.3" - "@firebase/component": "npm:0.6.10" - "@firebase/messaging-interop-types": "npm:0.2.2" - "@firebase/util": "npm:1.10.1" + "@firebase/app-check-interop-types": "npm:0.3.3" + "@firebase/auth-interop-types": "npm:0.2.4" + "@firebase/component": "npm:0.6.11" + "@firebase/messaging-interop-types": "npm:0.2.3" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app": 0.x - checksum: 10c0/93dd419bb08a20f7e2f2d1a599dcdb94b525f7d179df034c60fc60660a9b66ae2b5d564479c4b3044c4c346d2c585ceed64fa119b0bd0e91f18ccc0301983b80 + checksum: 10c0/6087dbdf7141a38cda35be995df5eb12b28b05e69c9df17496ec9a2046144195feb512bafea7d85b537e57d1bdccccf1e13dbabca29483ecca36b700e0fa5b4e languageName: node linkType: hard -"@firebase/installations-compat@npm:0.2.10": - version: 0.2.10 - resolution: "@firebase/installations-compat@npm:0.2.10" +"@firebase/installations-compat@npm:0.2.11": + version: 0.2.11 + resolution: "@firebase/installations-compat@npm:0.2.11" dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/installations": "npm:0.6.10" - "@firebase/installations-types": "npm:0.5.2" - "@firebase/util": "npm:1.10.1" + "@firebase/component": "npm:0.6.11" + "@firebase/installations": "npm:0.6.11" + "@firebase/installations-types": "npm:0.5.3" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app-compat": 0.x - checksum: 10c0/a7c32dcc2859977b75641e242c0910eba4387660ed68b58f33cf7e49b1c705d2afcb8cb2f9defa51f9a46cebdc7becd6ca8b2ad70151f6cf9710fa154d7d6743 + checksum: 10c0/3cab30c2c9c8db37e34d9b6b79022145fe2ae5ad71edff6ca880e6dbe020bb06c03757f5a69642b73c25242d2d1b92d14f65f8a2ab10b6f29c20602fff7faa4e languageName: node linkType: hard -"@firebase/installations-types@npm:0.5.2": - version: 0.5.2 - resolution: "@firebase/installations-types@npm:0.5.2" +"@firebase/installations-types@npm:0.5.3": + version: 0.5.3 + resolution: "@firebase/installations-types@npm:0.5.3" peerDependencies: "@firebase/app-types": 0.x - checksum: 10c0/f0a80b57fbeea6a079bfa564a8e5490aeb4a11e0d8e6ea73e548e3ccee637554eed30abc2c7c639d4fcc13c56f440f3aac1ff1588886cbaf552da0cbbd349545 + checksum: 10c0/f8af07a17e9c0cd1738009b880579b57d112f991ac99e4a17f327d89ad9f8f633fd50757bfd97f470edcc62045526dc59432fb7fcb1f76daa3c72c975519af62 languageName: node linkType: hard -"@firebase/installations@npm:0.6.10": - version: 0.6.10 - resolution: "@firebase/installations@npm:0.6.10" +"@firebase/installations@npm:0.6.11": + version: 0.6.11 + resolution: "@firebase/installations@npm:0.6.11" dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/util": "npm:1.10.1" + "@firebase/component": "npm:0.6.11" + "@firebase/util": "npm:1.10.2" idb: "npm:7.1.1" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app": 0.x - checksum: 10c0/d08af6a1a037f11da19f36d2fd687d33d6e1df7ec8ca5418e7ba5bb4382a9b320460dda6b3236143909ec0c01f2547b45eb08930e66d3f887f2f3acc52b13405 + checksum: 10c0/4af7d5d7d9c4a0792a3ecfef510410b426beec085ae8cc6ae71b79fec47c68976239744c004d0239f5c759005f32cd5fb35d80c0f4725d8b47b4970ec5745ce0 languageName: node linkType: hard -"@firebase/logger@npm:0.4.2": - version: 0.4.2 - resolution: "@firebase/logger@npm:0.4.2" +"@firebase/logger@npm:0.4.4": + version: 0.4.4 + resolution: "@firebase/logger@npm:0.4.4" dependencies: tslib: "npm:^2.1.0" - checksum: 10c0/bec040b451ac10fa2dbec54e262093eedab7a684d2f2c80f2549e918db6c4b2091ff7fc1f70f6cd1ec65564dc3b8f9b9d1b4dbfb9708b7ae2b9fd856ee764b3a + checksum: 10c0/0493468960c1243bad71ff932fbf89c17870b07cd3cb25b9565661689e52e93948e43cbd423f9903bdd80c40b98c28e4b2d85698e9ef09d4c59e23beb9140bda languageName: node linkType: hard -"@firebase/logger@npm:0.4.3": - version: 0.4.3 - resolution: "@firebase/logger@npm:0.4.3" +"@firebase/messaging-compat@npm:0.2.14": + version: 0.2.14 + resolution: "@firebase/messaging-compat@npm:0.2.14" dependencies: - tslib: "npm:^2.1.0" - checksum: 10c0/da4ff7a385fc2f12d02c156c00ffe504b8af9d3299ccde488055dbc11fa1c7283135f95583fdee2b5a3287e8a7ab0312d0f5f708619947edbcd5f9c15bac149a - languageName: node - linkType: hard - -"@firebase/messaging-compat@npm:0.2.13": - version: 0.2.13 - resolution: "@firebase/messaging-compat@npm:0.2.13" - dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/messaging": "npm:0.12.13" - "@firebase/util": "npm:1.10.1" + "@firebase/component": "npm:0.6.11" + "@firebase/messaging": "npm:0.12.14" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app-compat": 0.x - checksum: 10c0/0986e1ac07afe8698cc5effa487ce735092b81f1c0838c8d00c094f63fcba48f8511a6916e4ca23be0c14f5d81003bf4ba4bd640ebe5052421b48fe3cbbf3ac0 + checksum: 10c0/3abb4d47ef35e4458c3b1f38b063c90fd8b1eecb47fdcb0c3eab858a65db0f146ac11bc690f0623234ed5a3dd5479a4a8d78c33603cddbcff70f00148561b086 languageName: node linkType: hard -"@firebase/messaging-interop-types@npm:0.2.2": - version: 0.2.2 - resolution: "@firebase/messaging-interop-types@npm:0.2.2" - checksum: 10c0/c2ecebd2c1762869adc5a8dffc8881cb96ed4da8532291d6d5aca5302201546a19cd9a369561de29d253deb82d53be05e3d6fbdabd66ef1ba7c2e162ac5bf0f5 +"@firebase/messaging-interop-types@npm:0.2.3": + version: 0.2.3 + resolution: "@firebase/messaging-interop-types@npm:0.2.3" + checksum: 10c0/a6fb8f02db6a93f277cb5bd530934509e49465f775f2b5ed159116d9ce30b6255213781639b98984ff8b424a8fc36a8e5779e0cc3f0cf5e1bdbd41ae938d6c39 languageName: node linkType: hard -"@firebase/messaging@npm:0.12.13": - version: 0.12.13 - resolution: "@firebase/messaging@npm:0.12.13" +"@firebase/messaging@npm:0.12.14": + version: 0.12.14 + resolution: "@firebase/messaging@npm:0.12.14" dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/installations": "npm:0.6.10" - "@firebase/messaging-interop-types": "npm:0.2.2" - "@firebase/util": "npm:1.10.1" + "@firebase/component": "npm:0.6.11" + "@firebase/installations": "npm:0.6.11" + "@firebase/messaging-interop-types": "npm:0.2.3" + "@firebase/util": "npm:1.10.2" idb: "npm:7.1.1" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app": 0.x - checksum: 10c0/3a913b2772bc3d5a2495fd53abb31b8b6c88790ef4635362c6413663099504126d3319cd19854b3107a2956735c4a40e5bff265012e8068daa7aa633ab9af891 + checksum: 10c0/1ba76fb898e9a93f05540363c89da405d6b5e929b6e431daec98bb5116347f9aef59955496d4c4e3f9831c6ee651bbb11d6e43102199f29b6e3363b76299fb19 languageName: node linkType: hard -"@firebase/performance-compat@npm:0.2.10": - version: 0.2.10 - resolution: "@firebase/performance-compat@npm:0.2.10" +"@firebase/performance-compat@npm:0.2.11": + version: 0.2.11 + resolution: "@firebase/performance-compat@npm:0.2.11" dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/logger": "npm:0.4.3" - "@firebase/performance": "npm:0.6.10" - "@firebase/performance-types": "npm:0.2.2" - "@firebase/util": "npm:1.10.1" + "@firebase/component": "npm:0.6.11" + "@firebase/logger": "npm:0.4.4" + "@firebase/performance": "npm:0.6.11" + "@firebase/performance-types": "npm:0.2.3" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app-compat": 0.x - checksum: 10c0/644c0746f3b4080c3ee23e3fb6ac17920de33c6211091537ad601725592e0a27f93c567513f6499eff7a2c0f67412298afc1219b05e859bcc01ce8a1f9cd403c + checksum: 10c0/6a84e73d6a92cd892b0d6f6feaf0f65198f2660c8c0774d1d5202680563858ba44a4467d0d8ad688e9cbba77e58b9a731cb5756efccf3db6622a2b42265cde1a languageName: node linkType: hard -"@firebase/performance-types@npm:0.2.2": - version: 0.2.2 - resolution: "@firebase/performance-types@npm:0.2.2" - checksum: 10c0/4187b2d8c49fa7b51bb8811fc25b31500d7e90b43ad48977a57eb77e461be963d4c102468b81471b04c30125270ea48399a4976f1ceb2ddabfe6e1ab901541d1 +"@firebase/performance-types@npm:0.2.3": + version: 0.2.3 + resolution: "@firebase/performance-types@npm:0.2.3" + checksum: 10c0/971d6bff448481dd5e8ff9d643e14b364ed4d619aca1d8d64105555c7f4566c9c05bca3cd0c027b3f879cccf8c7bc0e31579f7f0d7b8b1de182af804572b2374 languageName: node linkType: hard -"@firebase/performance@npm:0.6.10": - version: 0.6.10 - resolution: "@firebase/performance@npm:0.6.10" +"@firebase/performance@npm:0.6.11": + version: 0.6.11 + resolution: "@firebase/performance@npm:0.6.11" dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/installations": "npm:0.6.10" - "@firebase/logger": "npm:0.4.3" - "@firebase/util": "npm:1.10.1" + "@firebase/component": "npm:0.6.11" + "@firebase/installations": "npm:0.6.11" + "@firebase/logger": "npm:0.4.4" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app": 0.x - checksum: 10c0/baf7ffd9d5967908a9a4dc0237b92e9b5e727df61d8d77756db230a0994238c04f62d107019cbc766b0ccd4f90926557881ca69a06358cbcfe5a9c78b7effd8e + checksum: 10c0/4a4788d212e0cd7cdd2fe5623d71b7feac177fb4567f750ed23f0994ea960f77f8beb7051721e77fc44b6f40194aca33567c6c2139aa576736df36ea4934a608 languageName: node linkType: hard -"@firebase/remote-config-compat@npm:0.2.10": - version: 0.2.10 - resolution: "@firebase/remote-config-compat@npm:0.2.10" +"@firebase/remote-config-compat@npm:0.2.11": + version: 0.2.11 + resolution: "@firebase/remote-config-compat@npm:0.2.11" dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/logger": "npm:0.4.3" - "@firebase/remote-config": "npm:0.4.10" - "@firebase/remote-config-types": "npm:0.3.2" - "@firebase/util": "npm:1.10.1" + "@firebase/component": "npm:0.6.11" + "@firebase/logger": "npm:0.4.4" + "@firebase/remote-config": "npm:0.4.11" + "@firebase/remote-config-types": "npm:0.3.3" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app-compat": 0.x - checksum: 10c0/7ac6abd814f8eb199da1ee33a80296137cfe113e0814faeeb2ea5358b2726ea0e56668ff5b5e9ced49ce66f7b183a2a20dd68c17d950b4588e8b064a3947bae9 + checksum: 10c0/49e8ee380c7d20b98b5ea533fddb8125dbdb0f123ea40ceb17ad5a7148f432effeb2ac613c6f4ac2ec966eb6f00501da0d4c26594d414274774a99d60e9c733e languageName: node linkType: hard -"@firebase/remote-config-types@npm:0.3.2": - version: 0.3.2 - resolution: "@firebase/remote-config-types@npm:0.3.2" - checksum: 10c0/eab1a2c046ed77a9072e73f9cb0a21ce8e93f79a726d6be06ff2338c608f4f3c98a10315ca151b6d88635da5c6301e2a6c8026db1828430a467259497380eb9b +"@firebase/remote-config-types@npm:0.3.3": + version: 0.3.3 + resolution: "@firebase/remote-config-types@npm:0.3.3" + checksum: 10c0/936ee3a5b673e424142d00e7a22788c3c6b28d068cc4fa690b203019f3f7586d1c5fe3cd520ea07744bf9ab93f25df44d0283efdb69611f6b8e02f102cdfd3eb languageName: node linkType: hard -"@firebase/remote-config@npm:0.4.10": - version: 0.4.10 - resolution: "@firebase/remote-config@npm:0.4.10" +"@firebase/remote-config@npm:0.4.11": + version: 0.4.11 + resolution: "@firebase/remote-config@npm:0.4.11" dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/installations": "npm:0.6.10" - "@firebase/logger": "npm:0.4.3" - "@firebase/util": "npm:1.10.1" + "@firebase/component": "npm:0.6.11" + "@firebase/installations": "npm:0.6.11" + "@firebase/logger": "npm:0.4.4" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app": 0.x - checksum: 10c0/c85febab2ee943484706bd37eaa343253a39b9099039de9f4610f2a4f03f4ec4d1c1e2424fad0e75f6803e06451cb5d13f9e098b7d1eac10c9ca09c9c65b099f + checksum: 10c0/6115001a7f5bd22aa1f8bb2e8c18321c53acccd7d7808555c0b414d44e26d99014a9d6f33d38e23d9949edcb6fd8bb23787326ba7fdb92b7a146841867629ed8 languageName: node linkType: hard -"@firebase/storage-compat@npm:0.3.13": - version: 0.3.13 - resolution: "@firebase/storage-compat@npm:0.3.13" +"@firebase/storage-compat@npm:0.3.14": + version: 0.3.14 + resolution: "@firebase/storage-compat@npm:0.3.14" dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/storage": "npm:0.13.3" - "@firebase/storage-types": "npm:0.8.2" - "@firebase/util": "npm:1.10.1" + "@firebase/component": "npm:0.6.11" + "@firebase/storage": "npm:0.13.4" + "@firebase/storage-types": "npm:0.8.3" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app-compat": 0.x - checksum: 10c0/4655c927156910b6d9ce2811e205b3d441b20917cb979ab51ae5c6ccbf751e0a188e628b1a8743f70182f05e030d0bdcb925cf1996f767978576c7a4e69bb5fb + checksum: 10c0/e48b147e886ae7985f16c819756306664204b9dfcff7f08545ee7446bc1490d65935b11739c125a968e8462a8302b55d2f3189afc48ef0bb3b2d49256fe6df6e languageName: node linkType: hard -"@firebase/storage-types@npm:0.8.2": - version: 0.8.2 - resolution: "@firebase/storage-types@npm:0.8.2" +"@firebase/storage-types@npm:0.8.3": + version: 0.8.3 + resolution: "@firebase/storage-types@npm:0.8.3" peerDependencies: "@firebase/app-types": 0.x "@firebase/util": 1.x - checksum: 10c0/8319975f6ee1585d52670fc75eaaf668ba9d4ae75c766dd1b33e609de68b191865a7125beeca5df6232636a7fd3a1cdc412848a1fc196b5410503f096de99daf + checksum: 10c0/4b34edca4fcbf75ba6575b02d823f5f5b0680977488a2e8101116313903d75973623cf4440f1e0f8048158e0804d0f5a7730f15bbe5af4ceb35fae6ff532a696 languageName: node linkType: hard -"@firebase/storage@npm:0.13.3": - version: 0.13.3 - resolution: "@firebase/storage@npm:0.13.3" +"@firebase/storage@npm:0.13.4": + version: 0.13.4 + resolution: "@firebase/storage@npm:0.13.4" dependencies: - "@firebase/component": "npm:0.6.10" - "@firebase/util": "npm:1.10.1" + "@firebase/component": "npm:0.6.11" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app": 0.x - checksum: 10c0/ec44f04197723056d1fba188a5ad29c77c7aafc11340153506c6943e338f15dcb61c8ab68ed7963735771cbcfec77e3db1c32cf86c13fd17cece653f9d9722e5 - languageName: node - linkType: hard - -"@firebase/util@npm:1.10.0": - version: 1.10.0 - resolution: "@firebase/util@npm:1.10.0" - dependencies: - tslib: "npm:^2.1.0" - checksum: 10c0/fc152a2cbdd06323f57f66c90cd388369e48e8910d589127f2ea76ca415c43c1c59b5b7b240307ae18f7f4c9cf0f97c71cb06e5ed8cba770b70958903ec52571 + checksum: 10c0/65d9286867a878f60271a5a51f8d6fa54a72672d741b7cb5b3263226b963c94986e976e0bd5b8aa82d9c5fe7d9a751e6f793a58a1f46130ab2d3d613379af9a8 languageName: node linkType: hard -"@firebase/util@npm:1.10.1": - version: 1.10.1 - resolution: "@firebase/util@npm:1.10.1" +"@firebase/util@npm:1.10.2": + version: 1.10.2 + resolution: "@firebase/util@npm:1.10.2" dependencies: tslib: "npm:^2.1.0" - checksum: 10c0/f128bcc97e31876f08e221ffc78d21fd5e4f6640e226d16a7247c850011d5a48fec4e9e8a44c0ab7fb2897b2c47e1d029a58723f24b0725c1fca0b8d5da96725 + checksum: 10c0/d6abb471948517cc9c560ebbb44e9e135716829c3abcd248a1af8aa111e48311ab410b693adc8f3bfe3b564896da7000dd7e26e34ecf59326f3b204a6a8b123c languageName: node linkType: hard -"@firebase/vertexai@npm:1.0.0": - version: 1.0.0 - resolution: "@firebase/vertexai@npm:1.0.0" +"@firebase/vertexai@npm:1.0.1": + version: 1.0.1 + resolution: "@firebase/vertexai@npm:1.0.1" dependencies: - "@firebase/app-check-interop-types": "npm:0.3.2" - "@firebase/component": "npm:0.6.10" - "@firebase/logger": "npm:0.4.3" - "@firebase/util": "npm:1.10.1" + "@firebase/app-check-interop-types": "npm:0.3.3" + "@firebase/component": "npm:0.6.11" + "@firebase/logger": "npm:0.4.4" + "@firebase/util": "npm:1.10.2" tslib: "npm:^2.1.0" peerDependencies: "@firebase/app": 0.x "@firebase/app-types": 0.x - checksum: 10c0/c7982f553e503c69679f67b1f8e7f8da5bad912a723790c4f2e809afcbe83860d84e41417e5095619b020e9ff06e87a22155f3eefbb0c1d1d43d24f25cdd13d8 + checksum: 10c0/3a56bb78500d05808cb8727cea2bdd4470c22d346dcb291d2772b48761d550f6d30939c0de36f40786b05790e3f348fdd037b4a8209b7b0bf00fc8f3962211e6 languageName: node linkType: hard -"@firebase/webchannel-wrapper@npm:1.0.2": - version: 1.0.2 - resolution: "@firebase/webchannel-wrapper@npm:1.0.2" - checksum: 10c0/b566bb131f10aed501e12b639810fd2a10577b4f0585034f224122e8becae771f0600eb6e017322bf09b5eb1916f8dbeabcf8831b9bdcb7a9671f0a00f3c345d +"@firebase/webchannel-wrapper@npm:1.0.3": + version: 1.0.3 + resolution: "@firebase/webchannel-wrapper@npm:1.0.3" + checksum: 10c0/faa1e53ea82ab6bda0b9dcc5f525101a301c74d1cffb924269de947a46511a633662dd6ee8ca571470e06642b35a596625228c766f37cc2d657321edfc560d28 languageName: node linkType: hard @@ -1124,14 +1090,14 @@ __metadata: languageName: node linkType: hard -"@formatjs/ecma402-abstract@npm:2.2.3": - version: 2.2.3 - resolution: "@formatjs/ecma402-abstract@npm:2.2.3" +"@formatjs/ecma402-abstract@npm:2.2.4": + version: 2.2.4 + resolution: "@formatjs/ecma402-abstract@npm:2.2.4" dependencies: "@formatjs/fast-memoize": "npm:2.2.3" - "@formatjs/intl-localematcher": "npm:0.5.7" + "@formatjs/intl-localematcher": "npm:0.5.8" tslib: "npm:2" - checksum: 10c0/611d12bf320fc5c5b85cb2b57e3dcebe8490a51c6a0459c857c7a3560656cd2bdba5b117e9dd7cf174f5aa120c11eaad7a65a6783637b816caa59a1bc5c727f6 + checksum: 10c0/3f262533fa704ea7a1a7a8107deee2609774a242c621f8cb5dd4bf4c97abf2fc12f5aeda3f4ce85be18147c484a0ca87303dca6abef53290717e685c55eabd2d languageName: node linkType: hard @@ -1144,79 +1110,54 @@ __metadata: languageName: node linkType: hard -"@formatjs/icu-messageformat-parser@npm:2.9.3": - version: 2.9.3 - resolution: "@formatjs/icu-messageformat-parser@npm:2.9.3" - dependencies: - "@formatjs/ecma402-abstract": "npm:2.2.3" - "@formatjs/icu-skeleton-parser": "npm:1.8.7" - tslib: "npm:2" - checksum: 10c0/519b59f7b4cf90681315c5382f7fcd105eb1974486f0d62d9227b6d0498895114ccc818792c208baae1ef79571d93b0edb9914c676e5ab76924dddb7fd6c28a0 - languageName: node - linkType: hard - -"@formatjs/icu-skeleton-parser@npm:1.8.7": - version: 1.8.7 - resolution: "@formatjs/icu-skeleton-parser@npm:1.8.7" +"@formatjs/icu-messageformat-parser@npm:2.9.4": + version: 2.9.4 + resolution: "@formatjs/icu-messageformat-parser@npm:2.9.4" dependencies: - "@formatjs/ecma402-abstract": "npm:2.2.3" + "@formatjs/ecma402-abstract": "npm:2.2.4" + "@formatjs/icu-skeleton-parser": "npm:1.8.8" tslib: "npm:2" - checksum: 10c0/e29eb4151580f2d324e6591509dc4543e2326266fc209a08580c94d502acab14acc3560d98b3aaf9ffbd5ff8e2683601ff08c65b32886f22da015c31ca35c5d0 + checksum: 10c0/f1ed14ece7ef0abc9fb62e323b78c994fc772d346801ad5aaa9555e1a7d5c0fda791345f4f2e53a3223f0b82c1a4eaf9a83544c1c20cb39349d1a39bedcf1648 languageName: node linkType: hard -"@formatjs/intl-displaynames@npm:6.8.4": - version: 6.8.4 - resolution: "@formatjs/intl-displaynames@npm:6.8.4" +"@formatjs/icu-skeleton-parser@npm:1.8.8": + version: 1.8.8 + resolution: "@formatjs/icu-skeleton-parser@npm:1.8.8" dependencies: - "@formatjs/ecma402-abstract": "npm:2.2.3" - "@formatjs/intl-localematcher": "npm:0.5.7" + "@formatjs/ecma402-abstract": "npm:2.2.4" tslib: "npm:2" - checksum: 10c0/a19531ff138ecde23e85f15e1287135a860cd8fe5a721fabceab688bd1001cb2d6256c87bb8b103930aa6245a93a36e594efb3d1d298287003cf8eac783d088a + checksum: 10c0/5ad78a5682e83b973e6fed4fca68660b944c41d1e941f0c84d69ff3d10ae835330062dc0a2cf0d237d2675ad3463405061a3963c14c2b9d8d1c1911f892b1a8d languageName: node linkType: hard -"@formatjs/intl-listformat@npm:7.7.4": - version: 7.7.4 - resolution: "@formatjs/intl-listformat@npm:7.7.4" - dependencies: - "@formatjs/ecma402-abstract": "npm:2.2.3" - "@formatjs/intl-localematcher": "npm:0.5.7" - tslib: "npm:2" - checksum: 10c0/aedee92b8adcf9c84bc729bfeb6719b11532a5227719299926f8660a1896e02aa3672491cb3c04ba51401ab620ba878f93bd7931c91689a0a37a55501ba6f940 - languageName: node - linkType: hard - -"@formatjs/intl-localematcher@npm:0.5.7": - version: 0.5.7 - resolution: "@formatjs/intl-localematcher@npm:0.5.7" +"@formatjs/intl-localematcher@npm:0.5.8": + version: 0.5.8 + resolution: "@formatjs/intl-localematcher@npm:0.5.8" dependencies: tslib: "npm:2" - checksum: 10c0/1ae374ca146a0d7457794926eed808c99971628e594f704a42ae2540b1f38928b26cbf942a7bbcc2796cc9fe8d9d7a603ac422bd9b89b714d2f91b506da40792 + checksum: 10c0/7a660263986326b662d4cb537e8386331c34fda61fb830b105e6c62d49be58ace40728dae614883b27a41cec7b1df8b44f72f79e16e6028bfca65d398dc04f3b languageName: node linkType: hard -"@formatjs/intl@npm:2.10.14": - version: 2.10.14 - resolution: "@formatjs/intl@npm:2.10.14" +"@formatjs/intl@npm:3.0.1": + version: 3.0.1 + resolution: "@formatjs/intl@npm:3.0.1" dependencies: - "@formatjs/ecma402-abstract": "npm:2.2.3" "@formatjs/fast-memoize": "npm:2.2.3" - "@formatjs/icu-messageformat-parser": "npm:2.9.3" - "@formatjs/intl-displaynames": "npm:6.8.4" - "@formatjs/intl-listformat": "npm:7.7.4" - intl-messageformat: "npm:10.7.6" + "@formatjs/icu-messageformat-parser": "npm:2.9.4" + intl-messageformat: "npm:10.7.7" tslib: "npm:2" peerDependencies: - typescript: ^4.7 || 5 + typescript: 5 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/e0bb71c53206e20176b509956d78f3ff0df5ce19bde6b25e7717289c10aa12f1e7203df139510060571e6d335786cf03d30b9096dae02a9759398314c90f6e2e + checksum: 10c0/0a109656c1c6eada6fe36e281d9f126b07f36e111ed8e468422e90582dd8b4d70b7aee971e6c4b6ee91c5826eeebeb386a9503df574bd4eb1fee1420a6911fd3 languageName: node linkType: hard -"@google-cloud/firestore@npm:^7.7.0": +"@google-cloud/firestore@npm:^7.10.0": version: 7.10.0 resolution: "@google-cloud/firestore@npm:7.10.0" dependencies: @@ -1253,7 +1194,7 @@ __metadata: languageName: node linkType: hard -"@google-cloud/storage@npm:^7.7.0": +"@google-cloud/storage@npm:^7.14.0": version: 7.14.0 resolution: "@google-cloud/storage@npm:7.14.0" dependencies: @@ -1357,7 +1298,7 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/retry@npm:^0.4.0": +"@humanwhocodes/retry@npm:^0.4.1": version: 0.4.1 resolution: "@humanwhocodes/retry@npm:0.4.1" checksum: 10c0/be7bb6841c4c01d0b767d9bb1ec1c9359ee61421ce8ba66c249d035c5acdfd080f32d55a5c9e859cdd7868788b8935774f65b2caf24ec0b7bd7bf333791f063b @@ -1650,26 +1591,26 @@ __metadata: languageName: node linkType: hard -"@mui/core-downloads-tracker@npm:^6.1.6": - version: 6.1.6 - resolution: "@mui/core-downloads-tracker@npm:6.1.6" - checksum: 10c0/538c561dc46e040ebc5ea884428dccc427fdddbd3747890d96ae52648eed5f7dec4dc8294927b58ff4b7481c0a813dcb16b9d7b9b08cc43871d2d55ebd1a8002 +"@mui/core-downloads-tracker@npm:^6.1.8": + version: 6.1.8 + resolution: "@mui/core-downloads-tracker@npm:6.1.8" + checksum: 10c0/a77ac4849c8a0f3bb0eecfae758f277fbdef46ff269314f495719a87f34f54b860d45a4648e456abac33d98b8070649478dc5918d92379728e2ff90e2cc798e1 languageName: node linkType: hard -"@mui/icons-material@npm:6.1.6": - version: 6.1.6 - resolution: "@mui/icons-material@npm:6.1.6" +"@mui/icons-material@npm:6.1.8": + version: 6.1.8 + resolution: "@mui/icons-material@npm:6.1.8" dependencies: "@babel/runtime": "npm:^7.26.0" peerDependencies: - "@mui/material": ^6.1.6 + "@mui/material": ^6.1.8 "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: "@types/react": optional: true - checksum: 10c0/a6eb10be3cc356fd404febf29a3b26fa63b6b09d3148736fb05279954905186e9804869ff18220840ae92dbdeddfd407c2d0c72b9e165e01fd6bbc620b6b39d7 + checksum: 10c0/730dd2581e3bdfabb4085ed4675fd3fd49d0f03c22291f79d0f51b1fd4f23b4edccb8b16c0b424b5f81dd6398742f6c9d52cb1fd075927826669732c4a8a0a8c languageName: node linkType: hard @@ -1705,15 +1646,15 @@ __metadata: languageName: node linkType: hard -"@mui/material@npm:6.1.6": - version: 6.1.6 - resolution: "@mui/material@npm:6.1.6" +"@mui/material@npm:6.1.8": + version: 6.1.8 + resolution: "@mui/material@npm:6.1.8" dependencies: "@babel/runtime": "npm:^7.26.0" - "@mui/core-downloads-tracker": "npm:^6.1.6" - "@mui/system": "npm:^6.1.6" + "@mui/core-downloads-tracker": "npm:^6.1.8" + "@mui/system": "npm:^6.1.8" "@mui/types": "npm:^7.2.19" - "@mui/utils": "npm:^6.1.6" + "@mui/utils": "npm:^6.1.8" "@popperjs/core": "npm:^2.11.8" "@types/react-transition-group": "npm:^4.4.11" clsx: "npm:^2.1.1" @@ -1724,7 +1665,7 @@ __metadata: peerDependencies: "@emotion/react": ^11.5.0 "@emotion/styled": ^11.3.0 - "@mui/material-pigment-css": ^6.1.6 + "@mui/material-pigment-css": ^6.1.8 "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1737,7 +1678,7 @@ __metadata: optional: true "@types/react": optional: true - checksum: 10c0/b54c0b01f33f63a700ec7b13d615dd3a109497ee48a1af0f750f780112a7034fbecfcecf29ad67aa62ec12047d465fbcb243052d8680ce681240096fef1f8d63 + checksum: 10c0/c4515ae5df41538d0eada15d899d70e1c7be83f16ee3a5c582e099d750584351e4220fab47fbeb267cd90e87bb40de9931414f23d9e66577b8235d442794720b languageName: node linkType: hard @@ -1758,6 +1699,23 @@ __metadata: languageName: node linkType: hard +"@mui/private-theming@npm:^6.1.8": + version: 6.1.8 + resolution: "@mui/private-theming@npm:6.1.8" + dependencies: + "@babel/runtime": "npm:^7.26.0" + "@mui/utils": "npm:^6.1.8" + prop-types: "npm:^15.8.1" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/16425a9001d3038531036dc47f031a4f1175d99b07b788983ce9a5e0c3c063132c6d508af31d3d13c3e44bedb4aa8b2f0111c5eb609ca8e0a652f87237ec1f38 + languageName: node + linkType: hard + "@mui/styled-engine@npm:^6.1.6": version: 6.1.6 resolution: "@mui/styled-engine@npm:6.1.6" @@ -1781,6 +1739,57 @@ __metadata: languageName: node linkType: hard +"@mui/styled-engine@npm:^6.1.8": + version: 6.1.8 + resolution: "@mui/styled-engine@npm:6.1.8" + dependencies: + "@babel/runtime": "npm:^7.26.0" + "@emotion/cache": "npm:^11.13.1" + "@emotion/serialize": "npm:^1.3.2" + "@emotion/sheet": "npm:^1.4.0" + csstype: "npm:^3.1.3" + prop-types: "npm:^15.8.1" + peerDependencies: + "@emotion/react": ^11.4.1 + "@emotion/styled": ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + checksum: 10c0/4da513a6bc72a2875fc0d4a097db5141849b69a2c62b867a1ac45d3fe112c2c18abb835f0bdfbe4ffbe626bff2f0490f014ccd3a7db72ada6e3b0cca87af63de + languageName: node + linkType: hard + +"@mui/system@npm:6.1.8, @mui/system@npm:^6.1.8": + version: 6.1.8 + resolution: "@mui/system@npm:6.1.8" + dependencies: + "@babel/runtime": "npm:^7.26.0" + "@mui/private-theming": "npm:^6.1.8" + "@mui/styled-engine": "npm:^6.1.8" + "@mui/types": "npm:^7.2.19" + "@mui/utils": "npm:^6.1.8" + clsx: "npm:^2.1.1" + csstype: "npm:^3.1.3" + prop-types: "npm:^15.8.1" + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@types/react": + optional: true + checksum: 10c0/af7902a1a6664055b4f764020746403749148f88c050edf509f557fd9f0b1d4d86ee9478d78e6c0356129f09b4101a93a345b05b5aa00125d3c164b148275faf + languageName: node + linkType: hard + "@mui/system@npm:^6.1.6": version: 6.1.6 resolution: "@mui/system@npm:6.1.6" @@ -1841,6 +1850,26 @@ __metadata: languageName: node linkType: hard +"@mui/utils@npm:^6.1.8": + version: 6.1.8 + resolution: "@mui/utils@npm:6.1.8" + dependencies: + "@babel/runtime": "npm:^7.26.0" + "@mui/types": "npm:^7.2.19" + "@types/prop-types": "npm:^15.7.13" + clsx: "npm:^2.1.1" + prop-types: "npm:^15.8.1" + react-is: "npm:^18.3.1" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/7d2bfa4863456a5223ddf6a93d56cc4c64e9de0ebc947953a4c23e83f8c9257d02a572da7d8c2dd93dcea5db0d321b7c8bb1e154b26fa5f22663eb6a262726ab + languageName: node + linkType: hard + "@next/env@npm:15.0.3": version: 15.0.3 resolution: "@next/env@npm:15.0.3" @@ -2034,16 +2063,16 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/instrumentation-amqplib@npm:^0.42.0": - version: 0.42.0 - resolution: "@opentelemetry/instrumentation-amqplib@npm:0.42.0" +"@opentelemetry/instrumentation-amqplib@npm:^0.43.0": + version: 0.43.0 + resolution: "@opentelemetry/instrumentation-amqplib@npm:0.43.0" dependencies: "@opentelemetry/core": "npm:^1.8.0" - "@opentelemetry/instrumentation": "npm:^0.53.0" + "@opentelemetry/instrumentation": "npm:^0.54.0" "@opentelemetry/semantic-conventions": "npm:^1.27.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/bdbcce51161f026ed7d57ae8694738655d1c1f1c1308570d28d85c6d42ed1cffedf2dac8dac0ab00a3fa820908525171086a265d5c366cd4a372517d87fbdff5 + checksum: 10c0/76d0e22d2d2ac06e98474e5cc14dca4f35117bed14bf4f584a1f8a2bf3a91a448d5fd0c6ebebeb5102dba4962a3293b785e410429ccf952391e4f00f3602c5d1 languageName: node linkType: hard @@ -2184,6 +2213,18 @@ __metadata: languageName: node linkType: hard +"@opentelemetry/instrumentation-knex@npm:0.41.0": + version: 0.41.0 + resolution: "@opentelemetry/instrumentation-knex@npm:0.41.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.54.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/f9d1acdbbe83c428d4929dee468ed19ac758d86e99f6cf1481ee6f04cc4012c60a36f8d75875571c1e10dc486e995d26f2431ec70c37ed5effd78bec8b53ced1 + languageName: node + linkType: hard + "@opentelemetry/instrumentation-koa@npm:0.43.0": version: 0.43.0 resolution: "@opentelemetry/instrumentation-koa@npm:0.43.0" @@ -2299,6 +2340,19 @@ __metadata: languageName: node linkType: hard +"@opentelemetry/instrumentation-tedious@npm:0.15.0": + version: 0.15.0 + resolution: "@opentelemetry/instrumentation-tedious@npm:0.15.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.54.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/tedious": "npm:^4.0.14" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/2e1e86497a09511dd1271968ca5610008a985901076d17252e3c27875a85253394bea9b24387a913babfca15c54523b00ddcde2d252c7a5cc8d5952676c82ce7 + languageName: node + linkType: hard + "@opentelemetry/instrumentation-undici@npm:0.6.0": version: 0.6.0 resolution: "@opentelemetry/instrumentation-undici@npm:0.6.0" @@ -2566,22 +2620,23 @@ __metadata: languageName: node linkType: hard -"@rollup/plugin-commonjs@npm:26.0.1": - version: 26.0.1 - resolution: "@rollup/plugin-commonjs@npm:26.0.1" +"@rollup/plugin-commonjs@npm:28.0.1": + version: 28.0.1 + resolution: "@rollup/plugin-commonjs@npm:28.0.1" dependencies: "@rollup/pluginutils": "npm:^5.0.1" commondir: "npm:^1.0.1" estree-walker: "npm:^2.0.2" - glob: "npm:^10.4.1" + fdir: "npm:^6.2.0" is-reference: "npm:1.2.1" magic-string: "npm:^0.30.3" + picomatch: "npm:^4.0.2" peerDependencies: rollup: ^2.68.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true - checksum: 10c0/483290d327bdb4147584c37d73e47df2c717735f1902cd2f66ebc83c7b40ae10e5a8d5e626f24b76ad4ac489eab4a8c13869410aad663810848b0abc89a630cf + checksum: 10c0/15d73306f539763a4b0d5723a0be9099b56d07118ff12b4c7f4c04b26e762076706e9f88a45f131d639ed9b7bd52e51facf93f2ca265b994172677b48ca705fe languageName: node linkType: hard @@ -2615,49 +2670,45 @@ __metadata: languageName: node linkType: hard -"@sentry-internal/browser-utils@npm:8.37.1": - version: 8.37.1 - resolution: "@sentry-internal/browser-utils@npm:8.37.1" +"@sentry-internal/browser-utils@npm:8.41.0": + version: 8.41.0 + resolution: "@sentry-internal/browser-utils@npm:8.41.0" dependencies: - "@sentry/core": "npm:8.37.1" - "@sentry/types": "npm:8.37.1" - "@sentry/utils": "npm:8.37.1" - checksum: 10c0/1202346f296db00ef1833d7bf6c8ccf728e5017d0fa352da72d631b822d9a1adc29be30e5c3dac2f5962c349c507b6777a98d9535c806694139fa5e08d4b43a4 + "@sentry/core": "npm:8.41.0" + "@sentry/types": "npm:8.41.0" + checksum: 10c0/287f5a900093ee6a41f6dd574761222db3cbf94591957b8b9146ee206ff461fb00647602656b78d2132c6b3362699c751c4f500ade0ccc16e2faea2cf7235b13 languageName: node linkType: hard -"@sentry-internal/feedback@npm:8.37.1": - version: 8.37.1 - resolution: "@sentry-internal/feedback@npm:8.37.1" +"@sentry-internal/feedback@npm:8.41.0": + version: 8.41.0 + resolution: "@sentry-internal/feedback@npm:8.41.0" dependencies: - "@sentry/core": "npm:8.37.1" - "@sentry/types": "npm:8.37.1" - "@sentry/utils": "npm:8.37.1" - checksum: 10c0/a3fca317b47d74ee1d34cf8f9fb6765a777833c56432b1db99841811c556bd801f25c63fde3a47dfbe7100a4b45a65224eef1a7d12d3c7480483e8b38918d908 + "@sentry/core": "npm:8.41.0" + "@sentry/types": "npm:8.41.0" + checksum: 10c0/a6476b205fb5ce25dedcb19bf39273ed76d0026e17be9e4a1df43d3a91e395f76a0e47d8302ab3ecd176e7c9ed99893ffb2c759e57e59e326c9f4330bddda812 languageName: node linkType: hard -"@sentry-internal/replay-canvas@npm:8.37.1": - version: 8.37.1 - resolution: "@sentry-internal/replay-canvas@npm:8.37.1" +"@sentry-internal/replay-canvas@npm:8.41.0": + version: 8.41.0 + resolution: "@sentry-internal/replay-canvas@npm:8.41.0" dependencies: - "@sentry-internal/replay": "npm:8.37.1" - "@sentry/core": "npm:8.37.1" - "@sentry/types": "npm:8.37.1" - "@sentry/utils": "npm:8.37.1" - checksum: 10c0/8109f1b4aad8eedf0731f8d7221b7d51fccb6d4737cb43b103df5fa9b7fbc82b321443ab082491f3ee72a7c453681c4de128041298a8948d7d61e3b68f3467a1 + "@sentry-internal/replay": "npm:8.41.0" + "@sentry/core": "npm:8.41.0" + "@sentry/types": "npm:8.41.0" + checksum: 10c0/2ec23d499d81a465e8055705d177308d2ad02428b7b8c0b1aadb1a288e4e559270998d57ea1dd78e2c670cd347fa0dc9c6561c2bbf0c786a31d1cc9b02a2d1e8 languageName: node linkType: hard -"@sentry-internal/replay@npm:8.37.1": - version: 8.37.1 - resolution: "@sentry-internal/replay@npm:8.37.1" +"@sentry-internal/replay@npm:8.41.0": + version: 8.41.0 + resolution: "@sentry-internal/replay@npm:8.41.0" dependencies: - "@sentry-internal/browser-utils": "npm:8.37.1" - "@sentry/core": "npm:8.37.1" - "@sentry/types": "npm:8.37.1" - "@sentry/utils": "npm:8.37.1" - checksum: 10c0/88ee4e32874f20623efaff71531cbb451f5201cbd78faed5c09c6651d463c9970b5432e5deb0b02933131dc5d8c71b73104b7fa225a565a98b8436b752816359 + "@sentry-internal/browser-utils": "npm:8.41.0" + "@sentry/core": "npm:8.41.0" + "@sentry/types": "npm:8.41.0" + checksum: 10c0/0d7e0bb9f94157790d5137b274cd08e01c5b6f97890941e51502bc98896f8a26cbe32a4e25658f79ac594922c3c4d0bcb62e46c52f7dc361e1eb502ed2e769b8 languageName: node linkType: hard @@ -2668,18 +2719,17 @@ __metadata: languageName: node linkType: hard -"@sentry/browser@npm:8.37.1": - version: 8.37.1 - resolution: "@sentry/browser@npm:8.37.1" +"@sentry/browser@npm:8.41.0": + version: 8.41.0 + resolution: "@sentry/browser@npm:8.41.0" dependencies: - "@sentry-internal/browser-utils": "npm:8.37.1" - "@sentry-internal/feedback": "npm:8.37.1" - "@sentry-internal/replay": "npm:8.37.1" - "@sentry-internal/replay-canvas": "npm:8.37.1" - "@sentry/core": "npm:8.37.1" - "@sentry/types": "npm:8.37.1" - "@sentry/utils": "npm:8.37.1" - checksum: 10c0/1cf0844ac9d7731a639efbf0a48abb111a6d91250010154cf83b5219a01a0bc48cc2dba3fe7cbd2f21fb4d5aa67db4cc5c1eac8cd8160e20ed91d1b9df3d0af5 + "@sentry-internal/browser-utils": "npm:8.41.0" + "@sentry-internal/feedback": "npm:8.41.0" + "@sentry-internal/replay": "npm:8.41.0" + "@sentry-internal/replay-canvas": "npm:8.41.0" + "@sentry/core": "npm:8.41.0" + "@sentry/types": "npm:8.41.0" + checksum: 10c0/a0a8f263d6b2c0343af005f0f9415b5229e1024281e30f88c510ff802f9efc3052bd60ff010bf5645cd3ed67f00cbd5bb29191de493837c96bd33d7fd27efe8e languageName: node linkType: hard @@ -2785,32 +2835,30 @@ __metadata: languageName: node linkType: hard -"@sentry/core@npm:8.37.1": - version: 8.37.1 - resolution: "@sentry/core@npm:8.37.1" +"@sentry/core@npm:8.41.0": + version: 8.41.0 + resolution: "@sentry/core@npm:8.41.0" dependencies: - "@sentry/types": "npm:8.37.1" - "@sentry/utils": "npm:8.37.1" - checksum: 10c0/29f2f8c3aa029366888a3d24b86bc75046ac25f6d45270be5eda78ab9c84cd8a08d9af896c3232fad1975a3c58563f94a75f6bea76cf24879f7a5b96ef8a8a29 + "@sentry/types": "npm:8.41.0" + checksum: 10c0/915fc2b64deef6d54053830d86dbe46e00eb445c7e82da7d31915241aeb7cc4919430e91ef5e9f6c270d102911a692af1138d318fd4660fc65692fec92365100 languageName: node linkType: hard -"@sentry/nextjs@npm:8.37.1": - version: 8.37.1 - resolution: "@sentry/nextjs@npm:8.37.1" +"@sentry/nextjs@npm:8.41.0": + version: 8.41.0 + resolution: "@sentry/nextjs@npm:8.41.0" dependencies: "@opentelemetry/api": "npm:^1.9.0" "@opentelemetry/instrumentation-http": "npm:0.53.0" "@opentelemetry/semantic-conventions": "npm:^1.27.0" - "@rollup/plugin-commonjs": "npm:26.0.1" - "@sentry-internal/browser-utils": "npm:8.37.1" - "@sentry/core": "npm:8.37.1" - "@sentry/node": "npm:8.37.1" - "@sentry/opentelemetry": "npm:8.37.1" - "@sentry/react": "npm:8.37.1" - "@sentry/types": "npm:8.37.1" - "@sentry/utils": "npm:8.37.1" - "@sentry/vercel-edge": "npm:8.37.1" + "@rollup/plugin-commonjs": "npm:28.0.1" + "@sentry-internal/browser-utils": "npm:8.41.0" + "@sentry/core": "npm:8.41.0" + "@sentry/node": "npm:8.41.0" + "@sentry/opentelemetry": "npm:8.41.0" + "@sentry/react": "npm:8.41.0" + "@sentry/types": "npm:8.41.0" + "@sentry/vercel-edge": "npm:8.41.0" "@sentry/webpack-plugin": "npm:2.22.6" chalk: "npm:3.0.0" resolve: "npm:1.22.8" @@ -2818,19 +2866,19 @@ __metadata: stacktrace-parser: "npm:^0.1.10" peerDependencies: next: ^13.2.0 || ^14.0 || ^15.0.0-rc.0 - checksum: 10c0/cb9ffc22956ec006bc56458f174b87eb7a91fa1c41f10430b52ec6e16bc36898cf22984dc06b9c421ac562982eaf2159db2a5be4b9416ef6110a48c23cff50bb + checksum: 10c0/0366521ea6cedbc0b71198e97c44d4cb172ff00b9c7dab69b3513efe7e5bb1b5def927256bf02edfa4b0b65891e6b7a96042af1d6a9438850cc3be989047c3de languageName: node linkType: hard -"@sentry/node@npm:8.37.1": - version: 8.37.1 - resolution: "@sentry/node@npm:8.37.1" +"@sentry/node@npm:8.41.0": + version: 8.41.0 + resolution: "@sentry/node@npm:8.41.0" dependencies: "@opentelemetry/api": "npm:^1.9.0" "@opentelemetry/context-async-hooks": "npm:^1.25.1" "@opentelemetry/core": "npm:^1.25.1" "@opentelemetry/instrumentation": "npm:^0.54.0" - "@opentelemetry/instrumentation-amqplib": "npm:^0.42.0" + "@opentelemetry/instrumentation-amqplib": "npm:^0.43.0" "@opentelemetry/instrumentation-connect": "npm:0.40.0" "@opentelemetry/instrumentation-dataloader": "npm:0.12.0" "@opentelemetry/instrumentation-express": "npm:0.44.0" @@ -2842,6 +2890,7 @@ __metadata: "@opentelemetry/instrumentation-http": "npm:0.53.0" "@opentelemetry/instrumentation-ioredis": "npm:0.43.0" "@opentelemetry/instrumentation-kafkajs": "npm:0.4.0" + "@opentelemetry/instrumentation-knex": "npm:0.41.0" "@opentelemetry/instrumentation-koa": "npm:0.43.0" "@opentelemetry/instrumentation-lru-memoizer": "npm:0.40.0" "@opentelemetry/instrumentation-mongodb": "npm:0.48.0" @@ -2851,77 +2900,65 @@ __metadata: "@opentelemetry/instrumentation-nestjs-core": "npm:0.40.0" "@opentelemetry/instrumentation-pg": "npm:0.44.0" "@opentelemetry/instrumentation-redis-4": "npm:0.42.0" + "@opentelemetry/instrumentation-tedious": "npm:0.15.0" "@opentelemetry/instrumentation-undici": "npm:0.6.0" "@opentelemetry/resources": "npm:^1.26.0" "@opentelemetry/sdk-trace-base": "npm:^1.26.0" "@opentelemetry/semantic-conventions": "npm:^1.27.0" "@prisma/instrumentation": "npm:5.19.1" - "@sentry/core": "npm:8.37.1" - "@sentry/opentelemetry": "npm:8.37.1" - "@sentry/types": "npm:8.37.1" - "@sentry/utils": "npm:8.37.1" + "@sentry/core": "npm:8.41.0" + "@sentry/opentelemetry": "npm:8.41.0" + "@sentry/types": "npm:8.41.0" import-in-the-middle: "npm:^1.11.2" - checksum: 10c0/8961ebb4e4bc29a8549769b7f83432ceb48106dc24155eb15d65d064a97a57902b6d61224408348b30e141d20a674fdd294d6831e970d08c3d3d425e1610efb7 + checksum: 10c0/df5e1b17eb03e8b60f3f35ce63da8d236e7bab0dd0482710e936ca0995caaa328e0d2da513c8442f60ac06b754c2bfbc5871fe1e0492f6cc12eb0e8163aaae31 languageName: node linkType: hard -"@sentry/opentelemetry@npm:8.37.1": - version: 8.37.1 - resolution: "@sentry/opentelemetry@npm:8.37.1" +"@sentry/opentelemetry@npm:8.41.0": + version: 8.41.0 + resolution: "@sentry/opentelemetry@npm:8.41.0" dependencies: - "@sentry/core": "npm:8.37.1" - "@sentry/types": "npm:8.37.1" - "@sentry/utils": "npm:8.37.1" + "@sentry/core": "npm:8.41.0" + "@sentry/types": "npm:8.41.0" peerDependencies: "@opentelemetry/api": ^1.9.0 "@opentelemetry/core": ^1.25.1 "@opentelemetry/instrumentation": ^0.54.0 "@opentelemetry/sdk-trace-base": ^1.26.0 "@opentelemetry/semantic-conventions": ^1.27.0 - checksum: 10c0/b233e9665b56dc578fefbd22edd715ea0a6f274013740d773cfb23dc57ece75bf6abcc71e4f39803b35fc56eec0e7ed5639fd26c76adccd864cb67a0c3a8c11d + checksum: 10c0/d817dd977e4730d4a5fe7330f43bf8fae1c73e9879158ec97cd03c7e9eddafc4ac775c755f62002e8795812563ee7acb2a4ef3a9bee53613a1ca489df7562904 languageName: node linkType: hard -"@sentry/react@npm:8.37.1": - version: 8.37.1 - resolution: "@sentry/react@npm:8.37.1" +"@sentry/react@npm:8.41.0": + version: 8.41.0 + resolution: "@sentry/react@npm:8.41.0" dependencies: - "@sentry/browser": "npm:8.37.1" - "@sentry/core": "npm:8.37.1" - "@sentry/types": "npm:8.37.1" - "@sentry/utils": "npm:8.37.1" + "@sentry/browser": "npm:8.41.0" + "@sentry/core": "npm:8.41.0" + "@sentry/types": "npm:8.41.0" hoist-non-react-statics: "npm:^3.3.2" peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x - checksum: 10c0/2930403dfb853340b7594fcb514599e512ecd2b0b6c8cfc42bb9ebaf9fff540f2391fafd7d960e53aa2130dd3875a902a8eed7b4f0ec0b72d1dbd1f438e78ae0 + checksum: 10c0/8db67897f68c75b7ec03579bda06f7a418a617a5e29bc110a9ee9e6675d8c91395a8c381aab595753928c9f80fd44e19a0397613bc285f9fedd19956726bd279 languageName: node linkType: hard -"@sentry/types@npm:8.37.1": - version: 8.37.1 - resolution: "@sentry/types@npm:8.37.1" - checksum: 10c0/f3cbd0c928e80f98002bda76b48b29dd089eb24eea33d3f0ddda1ffae1615fc16da6218fce43cc5ba637577c3d73bb34b7e9ce4c3545deb6a9d2929008a2b83d +"@sentry/types@npm:8.41.0": + version: 8.41.0 + resolution: "@sentry/types@npm:8.41.0" + checksum: 10c0/dd8643f63811802c8816fa9d6fbb495b646a4e271f66740dd95496a1f99278639298eb0c76dd555d7f6055516f9d7b4ad6aec447357748345a9b31373c87ce9f languageName: node linkType: hard -"@sentry/utils@npm:8.37.1": - version: 8.37.1 - resolution: "@sentry/utils@npm:8.37.1" - dependencies: - "@sentry/types": "npm:8.37.1" - checksum: 10c0/04d6ece0713d686028c8ab9fe9c082cf611365620010b2777f4dd2b41da7cbc9bcede5e9581e6569062bf5165f8a3f7602dfefd72ea73185bd57f16aeca6642a - languageName: node - linkType: hard - -"@sentry/vercel-edge@npm:8.37.1": - version: 8.37.1 - resolution: "@sentry/vercel-edge@npm:8.37.1" +"@sentry/vercel-edge@npm:8.41.0": + version: 8.41.0 + resolution: "@sentry/vercel-edge@npm:8.41.0" dependencies: "@opentelemetry/api": "npm:^1.9.0" - "@sentry/core": "npm:8.37.1" - "@sentry/types": "npm:8.37.1" - "@sentry/utils": "npm:8.37.1" - checksum: 10c0/9e06c9481a7ddbfc8a4d91b16ae37b061a50ef93ce1381f5bd6691ccae2d12c5119e011a8f49af1a8c09313c64241371f607a6e004e1ba7884c3f7ccbab53608 + "@sentry/core": "npm:8.41.0" + "@sentry/types": "npm:8.41.0" + checksum: 10c0/c88d3e3dc493c302fef853070f5a4792193265eb43f11a7cf7f60a305528ca75a71d8b33bc74d69296541d5426469d1343708a381a677bc0ee1c02e030a624c3 languageName: node linkType: hard @@ -3015,51 +3052,51 @@ __metadata: languageName: node linkType: hard -"@tanstack/eslint-plugin-query@npm:5.59.20": - version: 5.59.20 - resolution: "@tanstack/eslint-plugin-query@npm:5.59.20" +"@tanstack/eslint-plugin-query@npm:5.61.4": + version: 5.61.4 + resolution: "@tanstack/eslint-plugin-query@npm:5.61.4" dependencies: - "@typescript-eslint/utils": "npm:^8.3.0" + "@typescript-eslint/utils": "npm:^8.15.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - checksum: 10c0/95ded76098059b59ba7c4d47841c84a233b75e10b68261d01a5bce92afe1a70873e2fe91ef7650f9703aeb2d5f56fca29a56e7ee8d55c24f5b7437efdc2c80ea + checksum: 10c0/1cd38fb6f534d277baffebb4893ddc246a2fb041d97a4bfc3be465313ad3d92e31d683e0ad0ccc16858b167b330b0d72bb1743ba5fb92babd584d7b011d77ae7 languageName: node linkType: hard -"@tanstack/query-core@npm:5.59.20": - version: 5.59.20 - resolution: "@tanstack/query-core@npm:5.59.20" - checksum: 10c0/c93a8d41e21db532e92c1c90916bec578729d32d1f39d655603b9e81a9d5aebc8588a4d6a75928e04d3ddc90e71a1f8dc066a61d0ba24108eaf60cc2ce024a2f +"@tanstack/query-core@npm:5.61.5": + version: 5.61.5 + resolution: "@tanstack/query-core@npm:5.61.5" + checksum: 10c0/597af37641eb7e4123259f2f4244de977de98b1cab245a246f892b73bceac78d02e49f3368304e74691ab5ed74aa2b203b0a17045406f0303bbf962696856db9 languageName: node linkType: hard -"@tanstack/query-devtools@npm:5.59.20": - version: 5.59.20 - resolution: "@tanstack/query-devtools@npm:5.59.20" - checksum: 10c0/e90008af9c5754bacb19b78b54bbbb60b4ef4aeae8ff46349cbf8596f50a49b7a66db5b9c5fc4e5d9eba6d1fe4991f0178c1c4eacdc3bee5b8f4b48f50997174 +"@tanstack/query-devtools@npm:5.61.4": + version: 5.61.4 + resolution: "@tanstack/query-devtools@npm:5.61.4" + checksum: 10c0/44886dbf92849d17729bf779a184a3fd304711d8ce8061c1deccc089a65d168ed8f0cf0da43b59a9a22a9afb7b25fba99de8af47fe44479ac3082d852dcaaf53 languageName: node linkType: hard -"@tanstack/react-query-devtools@npm:5.59.20": - version: 5.59.20 - resolution: "@tanstack/react-query-devtools@npm:5.59.20" +"@tanstack/react-query-devtools@npm:5.61.5": + version: 5.61.5 + resolution: "@tanstack/react-query-devtools@npm:5.61.5" dependencies: - "@tanstack/query-devtools": "npm:5.59.20" + "@tanstack/query-devtools": "npm:5.61.4" peerDependencies: - "@tanstack/react-query": ^5.59.20 + "@tanstack/react-query": ^5.61.5 react: ^18 || ^19 - checksum: 10c0/bd49b0b66840dcf3e6939f78da9707f84d5c1da27a1e51e3ea1e389485d07829807b8b4ec11e11d4d30f709f7e82d732b121fa95715e809f312ef517c49cce7b + checksum: 10c0/62c7d105e6d5bc635cd0545654ebf7b6d95856d51423a3db686b86abc1c102b0bbe4d9d83f2a7b5111dc8f07a83b32b821cc37562ee9c3e45d74d5d1b198a06c languageName: node linkType: hard -"@tanstack/react-query@npm:5.59.20": - version: 5.59.20 - resolution: "@tanstack/react-query@npm:5.59.20" +"@tanstack/react-query@npm:5.61.5": + version: 5.61.5 + resolution: "@tanstack/react-query@npm:5.61.5" dependencies: - "@tanstack/query-core": "npm:5.59.20" + "@tanstack/query-core": "npm:5.61.5" peerDependencies: react: ^18 || ^19 - checksum: 10c0/fc3342c3a26c51c866d54082d14f86b2f644847ea8f9051000e529a012ea5437e18e35f999fcc453b2eecb0faaf29bd1aaec0956587ade63ba02262d6737800a + checksum: 10c0/1c535836025622a13f7a53947bc715147a34a5f98bcda32c0afd56f605073f21788b19f3e47e21876cad2cff7c9cd3b25cadd12eba5ddcc4c7e37f75d0742c32 languageName: node linkType: hard @@ -3067,25 +3104,25 @@ __metadata: version: 0.0.0-use.local resolution: "@tooling/eslint@workspace:tooling/eslint" dependencies: - "@eslint/compat": "npm:1.2.2" - "@eslint/eslintrc": "npm:3.1.0" - "@eslint/js": "npm:9.14.0" - "@tanstack/eslint-plugin-query": "npm:5.59.20" + "@eslint/compat": "npm:1.2.3" + "@eslint/eslintrc": "npm:3.2.0" + "@eslint/js": "npm:9.15.0" + "@tanstack/eslint-plugin-query": "npm:5.61.4" "@tooling/prettier": "workspace:*" "@tooling/staged": "workspace:^" "@tooling/typescript": "workspace:*" "@types/eslint": "npm:9.6.1" "@types/eslint__eslintrc": "npm:2.1.2" "@types/eslint__js": "npm:8.42.3" - eslint: "npm:9.14.0" + eslint: "npm:9.15.0" eslint-config-next: "npm:15.0.3" eslint-config-prettier: "npm:9.1.0" - eslint-plugin-turbo: "npm:2.2.3" + eslint-plugin-turbo: "npm:2.3.3" next: "npm:15.0.3" react: "npm:18.3.1" react-dom: "npm:18.3.1" - typescript: "npm:5.6.3" - typescript-eslint: "npm:8.13.0" + typescript: "npm:5.7.2" + typescript-eslint: "npm:8.16.0" bin: eslint-lint: ./bin/cli.js languageName: unknown @@ -3109,7 +3146,7 @@ __metadata: "@ianvs/prettier-plugin-sort-imports": "npm:4.4.0" "@tooling/staged": "workspace:^" "@tooling/typescript": "workspace:*" - prettier: "npm:3.3.3" + prettier: "npm:3.4.1" bin: prettier-format: ./bin/cli.js languageName: unknown @@ -3128,9 +3165,9 @@ __metadata: resolution: "@tooling/typescript@workspace:tooling/typescript" dependencies: "@total-typescript/ts-reset": "npm:0.6.1" - "@types/node": "npm:20.17.6" + "@types/node": "npm:22.10.1" network-information-types: "npm:0.1.1" - typescript: "npm:5.6.3" + typescript: "npm:5.7.2" languageName: unknown linkType: soft @@ -3340,7 +3377,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^22.0.1": +"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0": version: 22.9.0 resolution: "@types/node@npm:22.9.0" dependencies: @@ -3349,12 +3386,21 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:20.17.6": - version: 20.17.6 - resolution: "@types/node@npm:20.17.6" +"@types/node@npm:22.10.1": + version: 22.10.1 + resolution: "@types/node@npm:22.10.1" dependencies: - undici-types: "npm:~6.19.2" - checksum: 10c0/5918c7ff8368bbe6d06d5e739c8ae41a9db41628f28760c60cda797be7d233406f07c4d0e6fdd960a0a342ec4173c2217eb6624e06bece21c1f1dd1b92805c15 + undici-types: "npm:~6.20.0" + checksum: 10c0/0fbb6d29fa35d807f0223a4db709c598ac08d66820240a2cd6a8a69b8f0bc921d65b339d850a666b43b4e779f967e6ed6cf6f0fca3575e08241e6b900364c234 + languageName: node + linkType: hard + +"@types/node@npm:^22.8.7": + version: 22.9.1 + resolution: "@types/node@npm:22.9.1" + dependencies: + undici-types: "npm:~6.19.8" + checksum: 10c0/ea489ae603aa8874e4e88980aab6f2dad09c755da779c88dd142983bfe9609803c89415ca7781f723072934066f63daf2b3339ef084a8ad1a8079cf3958be243 languageName: node linkType: hard @@ -3485,6 +3531,15 @@ __metadata: languageName: node linkType: hard +"@types/tedious@npm:^4.0.14": + version: 4.0.14 + resolution: "@types/tedious@npm:4.0.14" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/d2914f8e9b5b998e4275ec5f0130cba1c2fb47e75616b5c125a65ef6c1db2f1dc3f978c7900693856a15d72bbb4f4e94f805537a4ecb6dc126c64415d31c0590 + languageName: node + linkType: hard + "@types/tough-cookie@npm:*": version: 4.0.5 resolution: "@types/tough-cookie@npm:4.0.5" @@ -3492,7 +3547,30 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.13.0, @typescript-eslint/eslint-plugin@npm:^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": +"@typescript-eslint/eslint-plugin@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.16.0" + dependencies: + "@eslint-community/regexpp": "npm:^4.10.0" + "@typescript-eslint/scope-manager": "npm:8.16.0" + "@typescript-eslint/type-utils": "npm:8.16.0" + "@typescript-eslint/utils": "npm:8.16.0" + "@typescript-eslint/visitor-keys": "npm:8.16.0" + graphemer: "npm:^1.4.0" + ignore: "npm:^5.3.1" + natural-compare: "npm:^1.4.0" + ts-api-utils: "npm:^1.3.0" + peerDependencies: + "@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/b03612b726ee5aff631cd50e05ceeb06a522e64465e4efdc134e3a27a09406b959ef7a05ec4acef1956b3674dc4fedb6d3a62ce69382f9e30c227bd4093003e5 + languageName: node + linkType: hard + +"@typescript-eslint/eslint-plugin@npm:^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": version: 8.13.0 resolution: "@typescript-eslint/eslint-plugin@npm:8.13.0" dependencies: @@ -3515,7 +3593,25 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.13.0, @typescript-eslint/parser@npm:^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": +"@typescript-eslint/parser@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/parser@npm:8.16.0" + dependencies: + "@typescript-eslint/scope-manager": "npm:8.16.0" + "@typescript-eslint/types": "npm:8.16.0" + "@typescript-eslint/typescript-estree": "npm:8.16.0" + "@typescript-eslint/visitor-keys": "npm:8.16.0" + debug: "npm:^4.3.4" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/e49c6640a7a863a16baecfbc5b99392a4731e9c7e9c9aaae4efbc354e305485fe0f39a28bf0acfae85bc01ce37fe0cc140fd315fdaca8b18f9b5e0addff8ceae + languageName: node + linkType: hard + +"@typescript-eslint/parser@npm:^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": version: 8.13.0 resolution: "@typescript-eslint/parser@npm:8.13.0" dependencies: @@ -3543,6 +3639,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/scope-manager@npm:8.16.0" + dependencies: + "@typescript-eslint/types": "npm:8.16.0" + "@typescript-eslint/visitor-keys": "npm:8.16.0" + checksum: 10c0/23b7c738b83f381c6419a36e6ca951944187e3e00abb8e012bce8041880410fe498303e28bdeb0e619023a69b14cf32a5ec1f9427c5382807788cd8e52a46a6e + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:8.13.0": version: 8.13.0 resolution: "@typescript-eslint/type-utils@npm:8.13.0" @@ -3558,6 +3664,23 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/type-utils@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/type-utils@npm:8.16.0" + dependencies: + "@typescript-eslint/typescript-estree": "npm:8.16.0" + "@typescript-eslint/utils": "npm:8.16.0" + debug: "npm:^4.3.4" + ts-api-utils: "npm:^1.3.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/24c0e815c8bdf99bf488c7528bd6a7c790e8b3b674cb7fb075663afc2ee26b48e6f4cf7c0d14bb21e2376ca62bd8525cbcb5688f36135b00b62b1d353d7235b9 + languageName: node + linkType: hard + "@typescript-eslint/types@npm:7.18.0": version: 7.18.0 resolution: "@typescript-eslint/types@npm:7.18.0" @@ -3572,6 +3695,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/types@npm:8.16.0" + checksum: 10c0/141e257ab4060a9c0e2e14334ca14ab6be713659bfa38acd13be70a699fb5f36932a2584376b063063ab3d723b24bc703dbfb1ce57d61d7cfd7ec5bd8a975129 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.13.0": version: 8.13.0 resolution: "@typescript-eslint/typescript-estree@npm:8.13.0" @@ -3591,6 +3721,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.16.0" + dependencies: + "@typescript-eslint/types": "npm:8.16.0" + "@typescript-eslint/visitor-keys": "npm:8.16.0" + debug: "npm:^4.3.4" + fast-glob: "npm:^3.3.2" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^1.3.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/f28fea5af4798a718b6735d1758b791a331af17386b83cb2856d89934a5d1693f7cb805e73c3b33f29140884ac8ead9931b1d7c3de10176fa18ca7a346fe10d0 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:^7.6.0": version: 7.18.0 resolution: "@typescript-eslint/typescript-estree@npm:7.18.0" @@ -3610,7 +3759,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.13.0, @typescript-eslint/utils@npm:^8.3.0": +"@typescript-eslint/utils@npm:8.13.0": version: 8.13.0 resolution: "@typescript-eslint/utils@npm:8.13.0" dependencies: @@ -3624,6 +3773,23 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:8.16.0, @typescript-eslint/utils@npm:^8.15.0": + version: 8.16.0 + resolution: "@typescript-eslint/utils@npm:8.16.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:8.16.0" + "@typescript-eslint/types": "npm:8.16.0" + "@typescript-eslint/typescript-estree": "npm:8.16.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/1e61187eef3da1ab1486d2a977d8f3b1cb8ef7fa26338500a17eb875ca42a8942ef3f2241f509eef74cf7b5620c109483afc7d83d5b0ab79b1e15920f5a49818 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:7.18.0": version: 7.18.0 resolution: "@typescript-eslint/visitor-keys@npm:7.18.0" @@ -3644,6 +3810,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.16.0" + dependencies: + "@typescript-eslint/types": "npm:8.16.0" + eslint-visitor-keys: "npm:^4.2.0" + checksum: 10c0/537df37801831aa8d91082b2adbffafd40305ed4518f0e7d3cbb17cc466d8b9ac95ac91fa232e7fe585d7c522d1564489ec80052ebb2a6ab9bbf89ef9dd9b7bc + languageName: node + linkType: hard + "@vue/compiler-core@npm:3.5.12": version: 3.5.12 resolution: "@vue/compiler-core@npm:3.5.12" @@ -3706,19 +3882,20 @@ __metadata: resolution: "@workspace/common@workspace:packages/common" dependencies: "@ackee/antonio-core": "npm:5.0.0" - "@emotion/react": "npm:11.13.3" + "@emotion/react": "npm:11.13.5" "@emotion/server": "npm:11.11.0" - "@emotion/styled": "npm:11.13.0" + "@emotion/styled": "npm:11.13.5" "@hookform/resolvers": "npm:3.9.1" - "@mui/icons-material": "npm:6.1.6" + "@mui/icons-material": "npm:6.1.8" "@mui/lab": "npm:6.0.0-beta.14" - "@mui/material": "npm:6.1.6" - "@sentry/nextjs": "npm:8.37.1" + "@mui/material": "npm:6.1.8" + "@mui/system": "npm:6.1.8" + "@sentry/nextjs": "npm:8.41.0" "@simplewebauthn/browser": "npm:11.0.0" "@simplewebauthn/server": "npm:11.0.0" "@t3-oss/env-nextjs": "npm:0.11.1" - "@tanstack/react-query": "npm:5.59.20" - "@tanstack/react-query-devtools": "npm:5.59.20" + "@tanstack/react-query": "npm:5.61.5" + "@tanstack/react-query-devtools": "npm:5.61.5" "@tooling/eslint": "workspace:*" "@tooling/madge": "workspace:*" "@tooling/prettier": "workspace:*" @@ -3726,21 +3903,22 @@ __metadata: "@types/react": "npm:18.3.12" "@types/react-dom": "npm:18.3.1" "@workspace/logger": "workspace:*" - cookie: "npm:1.0.1" + cookie: "npm:1.0.2" core-js: "npm:3.39.0" - firebase: "npm:11.0.1" - firebase-admin: "npm:12.7.0" + firebase: "npm:11.0.2" + firebase-admin: "npm:13.0.1" next: "npm:15.0.3" normalize.css: "npm:8.0.1" + nuqs: "npm:2.2.3" radash: "npm:12.1.0" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-hook-form: "npm:7.53.1" - react-intl: "npm:6.8.7" + react-hook-form: "npm:7.53.2" + react-intl: "npm:7.0.1" react-toastify: "npm:10.0.6" reset.css: "npm:2.0.2" tsc-alias: "npm:1.8.10" - typescript: "npm:5.6.3" + typescript: "npm:5.7.2" zod: "npm:3.23.8" languageName: unknown linkType: soft @@ -3749,17 +3927,31 @@ __metadata: version: 0.0.0-use.local resolution: "@workspace/logger@workspace:packages/logger" dependencies: - "@sentry/browser": "npm:8.37.1" "@tooling/eslint": "workspace:*" "@tooling/prettier": "workspace:*" "@tooling/typescript": "workspace:*" loglevel: "npm:1.9.2" peerDependencies: + "@sentry/nextjs": 8.x next: 15.x react: 18.x languageName: unknown linkType: soft +"@workspace/sentry@workspace:*, @workspace/sentry@workspace:packages/sentry": + version: 0.0.0-use.local + resolution: "@workspace/sentry@workspace:packages/sentry" + dependencies: + "@sentry/nextjs": "npm:8.41.0" + "@tooling/eslint": "workspace:*" + "@tooling/prettier": "workspace:*" + "@tooling/typescript": "workspace:*" + next: "npm:15.0.3" + react: "npm:18.3.1" + react-dom: "npm:18.3.1" + languageName: unknown + linkType: soft + "abbrev@npm:^2.0.0": version: 2.0.0 resolution: "abbrev@npm:2.0.0" @@ -4474,10 +4666,10 @@ __metadata: languageName: node linkType: hard -"cookie@npm:1.0.1": - version: 1.0.1 - resolution: "cookie@npm:1.0.1" - checksum: 10c0/80afdcad7fe9cab7a0ea1802629f6f4cf9ff957e9489daa7a813e3ac4ca842b0e5ab3f8e6a6ddc1f3f5c771b81c229afd6f0f3c083025d68c48d214ea8fb1097 +"cookie@npm:1.0.2": + version: 1.0.2 + resolution: "cookie@npm:1.0.2" + checksum: 10c0/fd25fe79e8fbcfcaf6aa61cd081c55d144eeeba755206c058682257cb38c4bd6795c6620de3f064c740695bb65b7949ebb1db7a95e4636efb8357a335ad3f54b languageName: node linkType: hard @@ -4517,14 +4709,14 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2": - version: 7.0.4 - resolution: "cross-spawn@npm:7.0.4" +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.5": + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" dependencies: path-key: "npm:^3.1.0" shebang-command: "npm:^2.0.0" which: "npm:^2.0.1" - checksum: 10c0/04f6c70dcbdd156f53073f13730f71160dabb91c8dfbdb24a873f4580ad7ca4b73c062ddfaaa2ba46d0dac433856d0cc0a07ff7173cd5404fefde952e87c9dbf + checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1 languageName: node linkType: hard @@ -5268,14 +5460,14 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-turbo@npm:2.2.3": - version: 2.2.3 - resolution: "eslint-plugin-turbo@npm:2.2.3" +"eslint-plugin-turbo@npm:2.3.3": + version: 2.3.3 + resolution: "eslint-plugin-turbo@npm:2.3.3" dependencies: dotenv: "npm:16.0.3" peerDependencies: eslint: ">6.6.0" - checksum: 10c0/37b88dc810e53cdd0b28cf381d89777db64828f00c4fd3ed72d9000624923f2388eabc1db1ace3414e9edec335f75e133a6fa60d80d35004487ddd1baa10fe5e + checksum: 10c0/dcad30a72cfb391b44a2f14f9d75b613330a315e4a1037a0cb16e36941dc8d4af1a86caa5c1d7ff8a9bbeea84fb1a5998c18690df0ada9afc416754410b4f18f languageName: node linkType: hard @@ -5303,25 +5495,25 @@ __metadata: languageName: node linkType: hard -"eslint@npm:9.14.0": - version: 9.14.0 - resolution: "eslint@npm:9.14.0" +"eslint@npm:9.15.0": + version: 9.15.0 + resolution: "eslint@npm:9.15.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.2.0" "@eslint-community/regexpp": "npm:^4.12.1" - "@eslint/config-array": "npm:^0.18.0" - "@eslint/core": "npm:^0.7.0" - "@eslint/eslintrc": "npm:^3.1.0" - "@eslint/js": "npm:9.14.0" - "@eslint/plugin-kit": "npm:^0.2.0" + "@eslint/config-array": "npm:^0.19.0" + "@eslint/core": "npm:^0.9.0" + "@eslint/eslintrc": "npm:^3.2.0" + "@eslint/js": "npm:9.15.0" + "@eslint/plugin-kit": "npm:^0.2.3" "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" - "@humanwhocodes/retry": "npm:^0.4.0" + "@humanwhocodes/retry": "npm:^0.4.1" "@types/estree": "npm:^1.0.6" "@types/json-schema": "npm:^7.0.15" ajv: "npm:^6.12.4" chalk: "npm:^4.0.0" - cross-spawn: "npm:^7.0.2" + cross-spawn: "npm:^7.0.5" debug: "npm:^4.3.2" escape-string-regexp: "npm:^4.0.0" eslint-scope: "npm:^8.2.0" @@ -5341,7 +5533,6 @@ __metadata: minimatch: "npm:^3.1.2" natural-compare: "npm:^1.4.0" optionator: "npm:^0.9.3" - text-table: "npm:^0.2.0" peerDependencies: jiti: "*" peerDependenciesMeta: @@ -5349,7 +5540,7 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10c0/e1cbf571b75519ad0b24c27e66a6575e57cab2671ef5296e7b345d9ac3adc1a549118dcc74a05b651a7a13a5e61ebb680be6a3e04a80e1f22eba1931921b5187 + checksum: 10c0/d0d7606f36bfcccb1c3703d0a24df32067b207a616f17efe5fb1765a91d13f085afffc4fc97ecde4ab9c9f4edd64d9b4ce750e13ff7937a25074b24bee15b20f languageName: node linkType: hard @@ -5517,6 +5708,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.2.0": + version: 6.4.2 + resolution: "fdir@npm:6.4.2" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/34829886f34a3ca4170eca7c7180ec4de51a3abb4d380344063c0ae2e289b11d2ba8b724afee974598c83027fea363ff598caf2b51bc4e6b1e0d8b80cc530573 + languageName: node + linkType: hard + "fetch-headers@npm:^2.0.0": version: 2.0.0 resolution: "fetch-headers@npm:2.0.0" @@ -5580,63 +5783,64 @@ __metadata: languageName: node linkType: hard -"firebase-admin@npm:12.7.0": - version: 12.7.0 - resolution: "firebase-admin@npm:12.7.0" +"firebase-admin@npm:13.0.1": + version: 13.0.1 + resolution: "firebase-admin@npm:13.0.1" dependencies: "@fastify/busboy": "npm:^3.0.0" - "@firebase/database-compat": "npm:1.0.8" - "@firebase/database-types": "npm:1.0.5" - "@google-cloud/firestore": "npm:^7.7.0" - "@google-cloud/storage": "npm:^7.7.0" - "@types/node": "npm:^22.0.1" + "@firebase/database-compat": "npm:^2.0.0" + "@firebase/database-types": "npm:^1.0.6" + "@google-cloud/firestore": "npm:^7.10.0" + "@google-cloud/storage": "npm:^7.14.0" + "@types/node": "npm:^22.8.7" farmhash-modern: "npm:^1.1.0" + google-auth-library: "npm:^9.14.2" jsonwebtoken: "npm:^9.0.0" jwks-rsa: "npm:^3.1.0" node-forge: "npm:^1.3.1" - uuid: "npm:^10.0.0" + uuid: "npm:^11.0.2" dependenciesMeta: "@google-cloud/firestore": optional: true "@google-cloud/storage": optional: true - checksum: 10c0/5a6645b004adbc13bce4d9876e8d62135408bdcf3537c32493832472bd219d543830a01da1773aa4183e298f96a476095613e6c8b1d334721e10313b6272da34 - languageName: node - linkType: hard - -"firebase@npm:11.0.1": - version: 11.0.1 - resolution: "firebase@npm:11.0.1" - dependencies: - "@firebase/analytics": "npm:0.10.9" - "@firebase/analytics-compat": "npm:0.2.15" - "@firebase/app": "npm:0.10.15" - "@firebase/app-check": "npm:0.8.9" - "@firebase/app-check-compat": "npm:0.3.16" - "@firebase/app-compat": "npm:0.2.45" - "@firebase/app-types": "npm:0.9.2" - "@firebase/auth": "npm:1.8.0" - "@firebase/auth-compat": "npm:0.5.15" - "@firebase/data-connect": "npm:0.1.1" - "@firebase/database": "npm:1.0.9" - "@firebase/database-compat": "npm:2.0.0" - "@firebase/firestore": "npm:4.7.4" - "@firebase/firestore-compat": "npm:0.3.39" - "@firebase/functions": "npm:0.11.9" - "@firebase/functions-compat": "npm:0.3.15" - "@firebase/installations": "npm:0.6.10" - "@firebase/installations-compat": "npm:0.2.10" - "@firebase/messaging": "npm:0.12.13" - "@firebase/messaging-compat": "npm:0.2.13" - "@firebase/performance": "npm:0.6.10" - "@firebase/performance-compat": "npm:0.2.10" - "@firebase/remote-config": "npm:0.4.10" - "@firebase/remote-config-compat": "npm:0.2.10" - "@firebase/storage": "npm:0.13.3" - "@firebase/storage-compat": "npm:0.3.13" - "@firebase/util": "npm:1.10.1" - "@firebase/vertexai": "npm:1.0.0" - checksum: 10c0/84320423426bed293d466d1c0f8ff808479d50081a7ff93965b4c926245f0e0f654b3157d4cbf3acc73094296b338575a086760a300a7a46652d252059a8bca7 + checksum: 10c0/0ecf6201141bdd765b0c47800f94bb2ece53d70db9e613127649cc370c4e6054305cde1b5eb573df41d13656eeccb3137392b23041e13e690626b7d37619c41d + languageName: node + linkType: hard + +"firebase@npm:11.0.2": + version: 11.0.2 + resolution: "firebase@npm:11.0.2" + dependencies: + "@firebase/analytics": "npm:0.10.10" + "@firebase/analytics-compat": "npm:0.2.16" + "@firebase/app": "npm:0.10.16" + "@firebase/app-check": "npm:0.8.10" + "@firebase/app-check-compat": "npm:0.3.17" + "@firebase/app-compat": "npm:0.2.46" + "@firebase/app-types": "npm:0.9.3" + "@firebase/auth": "npm:1.8.1" + "@firebase/auth-compat": "npm:0.5.16" + "@firebase/data-connect": "npm:0.1.2" + "@firebase/database": "npm:1.0.10" + "@firebase/database-compat": "npm:2.0.1" + "@firebase/firestore": "npm:4.7.5" + "@firebase/firestore-compat": "npm:0.3.40" + "@firebase/functions": "npm:0.11.10" + "@firebase/functions-compat": "npm:0.3.16" + "@firebase/installations": "npm:0.6.11" + "@firebase/installations-compat": "npm:0.2.11" + "@firebase/messaging": "npm:0.12.14" + "@firebase/messaging-compat": "npm:0.2.14" + "@firebase/performance": "npm:0.6.11" + "@firebase/performance-compat": "npm:0.2.11" + "@firebase/remote-config": "npm:0.4.11" + "@firebase/remote-config-compat": "npm:0.2.11" + "@firebase/storage": "npm:0.13.4" + "@firebase/storage-compat": "npm:0.3.14" + "@firebase/util": "npm:1.10.2" + "@firebase/vertexai": "npm:1.0.1" + checksum: 10c0/65a934e552d461b367e970fed0351b6c603d1d89fc9cd46e6e09521b5a23d6ff210b51af9ac160fff191cab7f87ae184319bc7becbf05e6355e72a60b4aeaf14 languageName: node linkType: hard @@ -5870,7 +6074,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.4.1": +"glob@npm:^10.2.2, glob@npm:^10.3.10": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -5961,6 +6165,20 @@ __metadata: languageName: node linkType: hard +"google-auth-library@npm:^9.14.2": + version: 9.15.0 + resolution: "google-auth-library@npm:9.15.0" + dependencies: + base64-js: "npm:^1.3.0" + ecdsa-sig-formatter: "npm:^1.0.11" + gaxios: "npm:^6.1.1" + gcp-metadata: "npm:^6.1.0" + gtoken: "npm:^7.0.0" + jws: "npm:^4.0.0" + checksum: 10c0/f5a9a46e939147b181bac9b254f11dd8c2d05c15a65c9d3f2180252bef21c12af37d9893bc3caacafd226d6531a960535dbb5222ef869143f393c6a97639cc06 + languageName: node + linkType: hard + "google-auth-library@npm:^9.3.0, google-auth-library@npm:^9.6.3": version: 9.14.2 resolution: "google-auth-library@npm:9.14.2" @@ -6169,12 +6387,12 @@ __metadata: languageName: node linkType: hard -"husky@npm:9.1.6": - version: 9.1.6 - resolution: "husky@npm:9.1.6" +"husky@npm:9.1.7": + version: 9.1.7 + resolution: "husky@npm:9.1.7" bin: husky: bin.js - checksum: 10c0/705673db4a247c1febd9c5df5f6a3519106cf0335845027bb50a15fba9b1f542cb2610932ede96fd08008f6d9f49db0f15560509861808b0031cdc0e7c798bac + checksum: 10c0/35bb110a71086c48906aa7cd3ed4913fb913823715359d65e32e0b964cb1e255593b0ae8014a5005c66a68e6fa66c38dcfa8056dbbdfb8b0187c0ffe7ee3a58f languageName: node linkType: hard @@ -6279,15 +6497,15 @@ __metadata: languageName: node linkType: hard -"intl-messageformat@npm:10.7.6": - version: 10.7.6 - resolution: "intl-messageformat@npm:10.7.6" +"intl-messageformat@npm:10.7.7": + version: 10.7.7 + resolution: "intl-messageformat@npm:10.7.7" dependencies: - "@formatjs/ecma402-abstract": "npm:2.2.3" + "@formatjs/ecma402-abstract": "npm:2.2.4" "@formatjs/fast-memoize": "npm:2.2.3" - "@formatjs/icu-messageformat-parser": "npm:2.9.3" + "@formatjs/icu-messageformat-parser": "npm:2.9.4" tslib: "npm:2" - checksum: 10c0/5e1309ed97523eafaf1bfb690b56441d4cb3ea9e62acdd7d7b5be56288b14752ce8570ce6e8238f275c846e3eaba6af23e537d0b85499a158592d512f21a0774 + checksum: 10c0/691895fb6a73a2feb2569658706e0d452861441de184dd1c9201e458a39fb80fc80080dd40d3d370400a52663f87de7a6d5a263c94245492f7265dd760441a95 languageName: node linkType: hard @@ -7309,6 +7527,13 @@ __metadata: languageName: node linkType: hard +"mitt@npm:^3.0.1": + version: 3.0.1 + resolution: "mitt@npm:3.0.1" + checksum: 10c0/3ab4fdecf3be8c5255536faa07064d05caa3dd332bd318ff02e04621f7b3069ca1de9106cfe8e7ced675abfc2bec2ce4c4ef321c4a1bb1fb29df8ae090741913 + languageName: node + linkType: hard + "mkdirp@npm:^1.0.3": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" @@ -7550,6 +7775,27 @@ __metadata: languageName: node linkType: hard +"nuqs@npm:2.2.3": + version: 2.2.3 + resolution: "nuqs@npm:2.2.3" + dependencies: + mitt: "npm:^3.0.1" + peerDependencies: + "@remix-run/react": ">=2" + next: ">=14.2.0" + react: ">=18.2.0 || ^19.0.0-0" + react-router-dom: ">=6" + peerDependenciesMeta: + "@remix-run/react": + optional: true + next: + optional: true + react-router-dom: + optional: true + checksum: 10c0/c240d5fb48d01832d747411da6137b5a42b9aceb2ac92042d6863133ca93d50726ebcafaaf993b995ea7bf4068329fe332a65ba61baa536c68cba05a6fc5f364 + languageName: node + linkType: hard + "object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" @@ -8032,12 +8278,12 @@ __metadata: languageName: node linkType: hard -"prettier@npm:3.3.3": - version: 3.3.3 - resolution: "prettier@npm:3.3.3" +"prettier@npm:3.4.1": + version: 3.4.1 + resolution: "prettier@npm:3.4.1" bin: prettier: bin/prettier.cjs - checksum: 10c0/b85828b08e7505716324e4245549b9205c0cacb25342a030ba8885aba2039a115dbcf75a0b7ca3b37bc9d101ee61fab8113fc69ca3359f2a226f1ecc07ad2e26 + checksum: 10c0/2d6cc3101ad9de72b49c59339480b0983e6ff6742143da0c43f476bf3b5ef88ede42ebd9956d7a0a8fa59f7a5990e8ef03c9ad4c37f7e4c9e5db43ee0853156c languageName: node linkType: hard @@ -8205,36 +8451,33 @@ __metadata: languageName: node linkType: hard -"react-hook-form@npm:7.53.1": - version: 7.53.1 - resolution: "react-hook-form@npm:7.53.1" +"react-hook-form@npm:7.53.2": + version: 7.53.2 + resolution: "react-hook-form@npm:7.53.2" peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - checksum: 10c0/dd2466359a633f873755b366d367d51ab17100566b687fb3b098f704232bc6ab1c79d29f879151e492880ca5eeac35e9425fbe5a309e2a55f7a4b5baf7826e8d + checksum: 10c0/18336d8e8798a70dcd0af703a0becca2d5dbf82a7b7a3ca334ae0e1f26410490bc3ef2ea51adcf790bb1e7006ed7a763fd00d664e398f71225b23529a7ccf0bf languageName: node linkType: hard -"react-intl@npm:6.8.7": - version: 6.8.7 - resolution: "react-intl@npm:6.8.7" +"react-intl@npm:7.0.1": + version: 7.0.1 + resolution: "react-intl@npm:7.0.1" dependencies: - "@formatjs/ecma402-abstract": "npm:2.2.3" - "@formatjs/icu-messageformat-parser": "npm:2.9.3" - "@formatjs/intl": "npm:2.10.14" - "@formatjs/intl-displaynames": "npm:6.8.4" - "@formatjs/intl-listformat": "npm:7.7.4" + "@formatjs/icu-messageformat-parser": "npm:2.9.4" + "@formatjs/intl": "npm:3.0.1" "@types/hoist-non-react-statics": "npm:3" "@types/react": "npm:16 || 17 || 18" hoist-non-react-statics: "npm:3" - intl-messageformat: "npm:10.7.6" + intl-messageformat: "npm:10.7.7" tslib: "npm:2" peerDependencies: react: ^16.6.0 || 17 || 18 - typescript: ^4.7 || 5 + typescript: 5 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/16cf57177e792bf27584c012389273d80642efecd0dbc2071906c824b58f99955c00970ab3e9bda25467fdc29b973eeeaf516887977d98ec72ae2cd1fc78d956 + checksum: 10c0/b2a6e7d566c75100fd0806ba3fb92c15c87f2e9d6ef662b7ed4052f088f1d4002fb942a6079d72b1df582b48567df5efa7444cbd2173c7c52a11da4f1588d2b5 languageName: node linkType: hard @@ -9170,13 +9413,6 @@ __metadata: languageName: node linkType: hard -"text-table@npm:^0.2.0": - version: 0.2.0 - resolution: "text-table@npm:0.2.0" - checksum: 10c0/02805740c12851ea5982686810702e2f14369a5f4c5c40a836821e3eefc65ffeec3131ba324692a37608294b0fd8c1e55a2dd571ffed4909822787668ddbee5c - languageName: node - linkType: hard - "through2@npm:~0.4.1": version: 0.4.2 resolution: "through2@npm:0.4.2" @@ -9277,58 +9513,58 @@ __metadata: languageName: node linkType: hard -"turbo-darwin-64@npm:2.2.3": - version: 2.2.3 - resolution: "turbo-darwin-64@npm:2.2.3" +"turbo-darwin-64@npm:2.3.3": + version: 2.3.3 + resolution: "turbo-darwin-64@npm:2.3.3" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"turbo-darwin-arm64@npm:2.2.3": - version: 2.2.3 - resolution: "turbo-darwin-arm64@npm:2.2.3" +"turbo-darwin-arm64@npm:2.3.3": + version: 2.3.3 + resolution: "turbo-darwin-arm64@npm:2.3.3" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"turbo-linux-64@npm:2.2.3": - version: 2.2.3 - resolution: "turbo-linux-64@npm:2.2.3" +"turbo-linux-64@npm:2.3.3": + version: 2.3.3 + resolution: "turbo-linux-64@npm:2.3.3" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"turbo-linux-arm64@npm:2.2.3": - version: 2.2.3 - resolution: "turbo-linux-arm64@npm:2.2.3" +"turbo-linux-arm64@npm:2.3.3": + version: 2.3.3 + resolution: "turbo-linux-arm64@npm:2.3.3" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"turbo-windows-64@npm:2.2.3": - version: 2.2.3 - resolution: "turbo-windows-64@npm:2.2.3" +"turbo-windows-64@npm:2.3.3": + version: 2.3.3 + resolution: "turbo-windows-64@npm:2.3.3" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"turbo-windows-arm64@npm:2.2.3": - version: 2.2.3 - resolution: "turbo-windows-arm64@npm:2.2.3" +"turbo-windows-arm64@npm:2.3.3": + version: 2.3.3 + resolution: "turbo-windows-arm64@npm:2.3.3" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"turbo@npm:2.2.3": - version: 2.2.3 - resolution: "turbo@npm:2.2.3" - dependencies: - turbo-darwin-64: "npm:2.2.3" - turbo-darwin-arm64: "npm:2.2.3" - turbo-linux-64: "npm:2.2.3" - turbo-linux-arm64: "npm:2.2.3" - turbo-windows-64: "npm:2.2.3" - turbo-windows-arm64: "npm:2.2.3" +"turbo@npm:2.3.3": + version: 2.3.3 + resolution: "turbo@npm:2.3.3" + dependencies: + turbo-darwin-64: "npm:2.3.3" + turbo-darwin-arm64: "npm:2.3.3" + turbo-linux-64: "npm:2.3.3" + turbo-linux-arm64: "npm:2.3.3" + turbo-windows-64: "npm:2.3.3" + turbo-windows-arm64: "npm:2.3.3" dependenciesMeta: turbo-darwin-64: optional: true @@ -9344,7 +9580,7 @@ __metadata: optional: true bin: turbo: bin/turbo - checksum: 10c0/5896cc1eb4b333ad11016f3b8a4ef5f13d3848c3b0abf4027edc53a0d10f3349bda5a7b880cc57ab1312b8c6371602f0ab932bc1c3902c65c674aef9c341af15 + checksum: 10c0/9aab52fb868a2b6246f41fe2343dec56c70252a9cb7729a3ea183458cfa728c1445d1b98882dd43542f0ffd46524e8ba7776fe0925e31a86904b113222778fa5 languageName: node linkType: hard @@ -9416,21 +9652,33 @@ __metadata: languageName: node linkType: hard -"typescript-eslint@npm:8.13.0": - version: 8.13.0 - resolution: "typescript-eslint@npm:8.13.0" +"typescript-eslint@npm:8.16.0": + version: 8.16.0 + resolution: "typescript-eslint@npm:8.16.0" dependencies: - "@typescript-eslint/eslint-plugin": "npm:8.13.0" - "@typescript-eslint/parser": "npm:8.13.0" - "@typescript-eslint/utils": "npm:8.13.0" + "@typescript-eslint/eslint-plugin": "npm:8.16.0" + "@typescript-eslint/parser": "npm:8.16.0" + "@typescript-eslint/utils": "npm:8.16.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/a84958e7602360c4cb2e6227fd9aae19dd18cdf1a2cfd9ece2a81d54098f80454b5707e861e98547d0b2e5dae552b136aa6733b74f0dd743ca7bfe178083c441 + checksum: 10c0/3da9401d6c2416b9d95c96a41a9423a5379d233a120cd3304e2c03f191d350ce91cf0c7e60017f7b10c93b4cc1190592702735735b771c1ce1bf68f71a9f1647 + languageName: node + linkType: hard + +"typescript@npm:5.7.2": + version: 5.7.2 + resolution: "typescript@npm:5.7.2" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/a873118b5201b2ef332127ef5c63fb9d9c155e6fdbe211cbd9d8e65877283797cca76546bad742eea36ed7efbe3424a30376818f79c7318512064e8625d61622 languageName: node linkType: hard -"typescript@npm:5.6.3, typescript@npm:^5.4.4, typescript@npm:^5.4.5, typescript@npm:^5.5.4": +"typescript@npm:^5.4.4, typescript@npm:^5.4.5, typescript@npm:^5.5.4": version: 5.6.3 resolution: "typescript@npm:5.6.3" bin: @@ -9440,7 +9688,17 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A5.6.3#optional!builtin, typescript@patch:typescript@npm%3A^5.4.4#optional!builtin, typescript@patch:typescript@npm%3A^5.4.5#optional!builtin, typescript@patch:typescript@npm%3A^5.5.4#optional!builtin": +"typescript@patch:typescript@npm%3A5.7.2#optional!builtin": + version: 5.7.2 + resolution: "typescript@patch:typescript@npm%3A5.7.2#optional!builtin::version=5.7.2&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/f3b8082c9d1d1629a215245c9087df56cb784f9fb6f27b5d55577a20e68afe2a889c040aacff6d27e35be165ecf9dca66e694c42eb9a50b3b2c451b36b5675cb + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A^5.4.4#optional!builtin, typescript@patch:typescript@npm%3A^5.4.5#optional!builtin, typescript@patch:typescript@npm%3A^5.5.4#optional!builtin": version: 5.6.3 resolution: "typescript@patch:typescript@npm%3A5.6.3#optional!builtin::version=5.6.3&hash=8c6c40" bin: @@ -9462,13 +9720,20 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.19.2, undici-types@npm:~6.19.8": +"undici-types@npm:~6.19.8": version: 6.19.8 resolution: "undici-types@npm:6.19.8" checksum: 10c0/078afa5990fba110f6824823ace86073b4638f1d5112ee26e790155f481f2a868cc3e0615505b6f4282bdf74a3d8caad715fd809e870c2bb0704e3ea6082f344 languageName: node linkType: hard +"undici-types@npm:~6.20.0": + version: 6.20.0 + resolution: "undici-types@npm:6.20.0" + checksum: 10c0/68e659a98898d6a836a9a59e6adf14a5d799707f5ea629433e025ac90d239f75e408e2e5ff086afc3cace26f8b26ee52155293564593fbb4a2f666af57fc59bf + languageName: node + linkType: hard + "unique-filename@npm:^3.0.0": version: 3.0.0 resolution: "unique-filename@npm:3.0.0" @@ -9529,12 +9794,12 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^10.0.0": - version: 10.0.0 - resolution: "uuid@npm:10.0.0" +"uuid@npm:^11.0.2": + version: 11.0.3 + resolution: "uuid@npm:11.0.3" bin: - uuid: dist/bin/uuid - checksum: 10c0/eab18c27fe4ab9fb9709a5d5f40119b45f2ec8314f8d4cf12ce27e4c6f4ffa4a6321dc7db6c515068fa373c075b49691ba969f0010bf37f44c37ca40cd6bf7fe + uuid: dist/esm/bin/uuid + checksum: 10c0/cee762fc76d949a2ff9205770334699e0043d52bb766472593a25f150077c9deed821230251ea3d6ab3943a5ea137d2826678797f1d5f6754c7ce5ce27e9f7a6 languageName: node linkType: hard @@ -9581,10 +9846,30 @@ __metadata: "@tooling/prettier": "workspace:*" "@tooling/typescript": "workspace:*" "@workspace/common": "workspace:*" + "@workspace/sentry": "workspace:*" + browserslist-config-custom: "workspace:*" + next: "npm:15.0.3" + react: "npm:18.3.1" + react-dom: "npm:18.3.1" + typescript: "npm:5.7.2" + languageName: unknown + linkType: soft + +"webauthn-upgrade-example@workspace:examples/webauthn-upgrade": + version: 0.0.0-use.local + resolution: "webauthn-upgrade-example@workspace:examples/webauthn-upgrade" + dependencies: + "@tooling/eslint": "workspace:*" + "@tooling/madge": "workspace:*" + "@tooling/prettier": "workspace:*" + "@tooling/typescript": "workspace:*" + "@workspace/common": "workspace:*" + "@workspace/sentry": "workspace:*" browserslist-config-custom: "workspace:*" next: "npm:15.0.3" react: "npm:18.3.1" react-dom: "npm:18.3.1" + typescript: "npm:5.7.2" languageName: unknown linkType: soft @@ -9725,8 +10010,8 @@ __metadata: "@tooling/prettier": "workspace:*" "@tooling/typescript": "workspace:*" dotenv: "npm:16.4.5" - husky: "npm:9.1.6" - turbo: "npm:2.2.3" + husky: "npm:9.1.7" + turbo: "npm:2.3.3" languageName: unknown linkType: soft