diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml
index a9ccfe2..8436226 100644
--- a/.github/workflows/e2e-tests.yml
+++ b/.github/workflows/e2e-tests.yml
@@ -1,4 +1,4 @@
-name: E2E Tests
+name: Build Test
on:
push:
@@ -7,7 +7,7 @@ on:
branches: [ main ]
jobs:
- e2e-tests:
+ build-test:
runs-on: ubuntu-latest
steps:
@@ -40,6 +40,12 @@ jobs:
- name: Install dependencies
run: pnpm install
+
+ - name: Build package
+ run: pnpm build
+
+ - name: Run publint
+ run: npx publint --errors-only
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..726e50c
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,20 @@
+# Build artifacts
+dist/
+node_modules/
+.git/
+.vscode/
+.idea/
+
+# Examples directory (not our focus)
+examples/
+
+# Test files (only focusing on src)
+tests/
+
+# Coverage reports
+coverage/
+
+# Lock files
+pnpm-lock.yaml
+package-lock.json
+yarn.lock
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..5e124ba
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,9 @@
+{
+ "semi": true,
+ "singleQuote": true,
+ "tabWidth": 2,
+ "trailingComma": "es5",
+ "printWidth": 80,
+ "arrowParens": "avoid",
+ "endOfLine": "lf"
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 5b7211a..47553b9 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,10 @@ export default defineConfig(() => {
pluginReactRouter({
// Optional: Enable custom server mode
customServer: false,
+ // Optional: Specify server output format
+ serverOutput: "commonjs",
+ //Optional: enable experimental support for module federation
+ federation: false
}),
pluginReact()
],
@@ -60,7 +64,19 @@ pluginReactRouter({
* Enable this when you want to handle server setup manually.
* @default false
*/
- customServer?: boolean
+ customServer?: boolean,
+
+ /**
+ * Specify the output format for server-side code.
+ * Options: "commonjs" | "module"
+ * @default "module"
+ */
+ serverOutput?: "commonjs" | "module"
+ /**
+ * Enable experimental support for module federation
+ * @default false
+ */
+ federation?: boolean
})
```
diff --git a/examples/cloudflare/package.json b/examples/cloudflare/package.json
index 5326569..f3e6a1b 100644
--- a/examples/cloudflare/package.json
+++ b/examples/cloudflare/package.json
@@ -21,8 +21,8 @@
"@cloudflare/workers-types": "^4.20241112.0",
"@react-router/cloudflare": "^7.4.0",
"@react-router/dev": "^7.4.0",
- "@rsbuild/core": "^1.2.3",
- "@rsbuild/plugin-react": "^1.1.0",
+ "@rsbuild/core": "^1.2.19",
+ "@rsbuild/plugin-react": "^1.1.1",
"@rsbuild/plugin-react-router": "workspace:*",
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^20",
diff --git a/examples/custom-node-server/package.json b/examples/custom-node-server/package.json
index 594a86a..c9a16be 100644
--- a/examples/custom-node-server/package.json
+++ b/examples/custom-node-server/package.json
@@ -8,7 +8,10 @@
"dev": "RSDOCTOR=false node server.js",
"start": "NODE_ENV=production node server.js",
"build": "rsbuild build",
- "typecheck": "react-router typegen && tsc"
+ "typecheck": "react-router typegen && tsc",
+ "test:e2e": "pnpm run dev & sleep 5 && playwright test",
+ "test:e2e:debug": "playwright test --debug",
+ "test:e2e:ui": "playwright test --ui"
},
"keywords": [],
"author": "",
@@ -23,9 +26,10 @@
"react-router": "^7.4.0"
},
"devDependencies": {
+ "@playwright/test": "^1.50.1",
"@react-router/dev": "^7.4.0",
- "@rsbuild/core": "^1.2.3",
- "@rsbuild/plugin-react": "^1.1.0",
+ "@rsbuild/core": "^1.2.19",
+ "@rsbuild/plugin-react": "^1.1.1",
"@rsbuild/plugin-react-router": "workspace:*",
"@rsdoctor/rspack-plugin": "^0.4.13",
"@types/express": "^5.0.0",
diff --git a/examples/custom-node-server/playwright.config.ts b/examples/custom-node-server/playwright.config.ts
new file mode 100644
index 0000000..d8874f2
--- /dev/null
+++ b/examples/custom-node-server/playwright.config.ts
@@ -0,0 +1,36 @@
+import { defineConfig, devices } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './tests/e2e',
+ // Maximum time one test can run for
+ timeout: 30 * 1000,
+ expect: {
+ timeout: 5000
+ },
+ // Run tests in files in parallel
+ fullyParallel: false,
+ // Fail the build on CI if you accidentally left test.only in the source code
+ forbidOnly: !!process.env.CI,
+ // Retry on CI only
+ retries: process.env.CI ? 2 : 0,
+
+ // Shared settings for all the projects below
+ use: {
+ // Base URL to use in actions like `await page.goto('/')`
+ baseURL: 'http://localhost:3000',
+
+ // Collect trace when retrying the failed test
+ trace: 'on-first-retry',
+
+ // Take screenshot on test failure
+ screenshot: 'only-on-failure',
+ },
+
+ // Configure only Chrome desktop browser
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ]
+});
\ No newline at end of file
diff --git a/examples/custom-node-server/server/app.ts b/examples/custom-node-server/server/app.ts
deleted file mode 100644
index 477feb4..0000000
--- a/examples/custom-node-server/server/app.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import 'react-router';
-import { createRequestHandler } from '@react-router/express';
-
-declare module 'react-router' {
- interface AppLoadContext {
- VALUE_FROM_EXPRESS: string;
- }
-}
-
-export const app = createRequestHandler({
- // @ts-expect-error - virtual module provided by React Router at build time
- build: () => import('virtual/react-router/server-build'),
- getLoadContext() {
- return {
- VALUE_FROM_EXPRESS: 'Hello from Express',
- };
- },
-});
diff --git a/examples/custom-node-server/server/index.ts b/examples/custom-node-server/server/index.ts
index 6b0143d..477feb4 100644
--- a/examples/custom-node-server/server/index.ts
+++ b/examples/custom-node-server/server/index.ts
@@ -7,7 +7,7 @@ declare module 'react-router' {
}
}
-export const index = createRequestHandler({
+export const app = createRequestHandler({
// @ts-expect-error - virtual module provided by React Router at build time
build: () => import('virtual/react-router/server-build'),
getLoadContext() {
diff --git a/examples/custom-node-server/test-results/.last-run.json b/examples/custom-node-server/test-results/.last-run.json
new file mode 100644
index 0000000..cbcc1fb
--- /dev/null
+++ b/examples/custom-node-server/test-results/.last-run.json
@@ -0,0 +1,4 @@
+{
+ "status": "passed",
+ "failedTests": []
+}
\ No newline at end of file
diff --git a/examples/custom-node-server/tests/e2e/README.md b/examples/custom-node-server/tests/e2e/README.md
new file mode 100644
index 0000000..3016274
--- /dev/null
+++ b/examples/custom-node-server/tests/e2e/README.md
@@ -0,0 +1,67 @@
+# End-to-End Tests
+
+This directory contains end-to-end tests for the React Router default template application using Playwright.
+
+## Test Structure
+
+The tests are organized by feature area:
+
+- `home.test.ts` - Tests for the home page and welcome component
+- `about.test.ts` - Tests for the about page
+- `docs.test.ts` - Tests for the docs section with nested routes
+- `projects.test.ts` - Tests for the projects section with dynamic routes
+- `navigation.test.ts` - General navigation flows across the application
+
+## Running Tests
+
+You can run the tests using the following npm scripts:
+
+```bash
+# Run all tests
+npm run test:e2e
+
+# Run tests with the Playwright UI
+npm run test:e2e:ui
+
+# Run tests in debug mode
+npm run test:e2e:debug
+```
+
+## Test Configuration
+
+Test configuration is defined in `playwright.config.ts` in the project root. The configuration:
+
+- Runs tests in the `tests/e2e` directory
+- Tests across multiple browsers (Chrome, Firefox, Safari)
+- Tests across desktop and mobile viewports
+- Automatically starts the development server before running tests
+- Takes screenshots on test failures
+- Generates HTML reports
+
+## Adding New Tests
+
+To add new tests:
+
+1. Create a new file in the `tests/e2e` directory with the `.test.ts` extension
+2. Import the required Playwright utilities:
+ ```typescript
+ import { test, expect } from '@playwright/test';
+ ```
+3. Write your tests using the Playwright API
+4. Run your tests with `npm run test:e2e`
+
+## Generating Base Screenshots
+
+If you need to generate baseline screenshots for visual comparison:
+
+```bash
+npx playwright test --update-snapshots
+```
+
+## CI Integration
+
+These tests can be integrated into CI pipelines. The configuration includes special settings for CI environments:
+
+- More retries on CI
+- Forbidding `.only` tests on CI
+- Not reusing existing servers on CI
\ No newline at end of file
diff --git a/examples/custom-node-server/tests/e2e/about.test.ts b/examples/custom-node-server/tests/e2e/about.test.ts
new file mode 100644
index 0000000..d59881c
--- /dev/null
+++ b/examples/custom-node-server/tests/e2e/about.test.ts
@@ -0,0 +1,46 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('About Page', () => {
+ test('should display about page content and team members', async ({ page }) => {
+ // Navigate to about page
+ await page.goto('/about');
+
+ // Check page heading
+ const heading = page.locator('h1:has-text("About This Demo")');
+ await expect(heading).toBeVisible();
+
+ // Check team member cards
+ const teamCards = page.locator('.card');
+ await expect(teamCards).toHaveCount(3);
+
+ // Verify each team member
+ const expectedMembers = ['React Router', 'Tailwind CSS', 'TypeScript'];
+ for (let i = 0; i < expectedMembers.length; i++) {
+ const memberName = expectedMembers[i];
+ await expect(teamCards.nth(i).locator('h2')).toContainText(memberName);
+ }
+
+ // Check that back to home link works
+ const backLink = page.locator('a:has-text("β Back to Home")');
+ await expect(backLink).toBeVisible();
+ await backLink.click();
+
+ // Verify navigation back to home page
+ await expect(page).toHaveURL(/\/$/);
+ await expect(page.locator('h1:has-text("Welcome to React Router")')).toBeVisible();
+ });
+
+ test('should have working external links', async ({ page }) => {
+ // Navigate to about page
+ await page.goto('/about');
+
+ // Get all external links
+ const externalLinks = page.locator('.card a[target="_blank"]');
+
+ // Verify each link has correct attributes
+ for (const link of await externalLinks.all()) {
+ await expect(link).toHaveAttribute('rel', 'noopener noreferrer');
+ await expect(link).toHaveText('Learn more β');
+ }
+ });
+});
\ No newline at end of file
diff --git a/examples/custom-node-server/tests/e2e/docs.test.ts b/examples/custom-node-server/tests/e2e/docs.test.ts
new file mode 100644
index 0000000..b673dde
--- /dev/null
+++ b/examples/custom-node-server/tests/e2e/docs.test.ts
@@ -0,0 +1,54 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Docs Section', () => {
+ test('should navigate through docs section with nested routes', async ({ page }) => {
+ // Navigate to docs index
+ await page.goto('/docs');
+
+ // Verify the docs index page is shown
+ await expect(page).toHaveURL('/docs');
+
+ // Navigate to getting-started page
+ await page.goto('/docs/getting-started');
+ await expect(page).toHaveURL('/docs/getting-started');
+
+ // Navigate to advanced page
+ await page.goto('/docs/advanced');
+ await expect(page).toHaveURL('/docs/advanced');
+
+ // Verify layouts are preserved during navigation
+ await page.goto('/docs');
+
+ // Check for the main navigation menu
+ const mainNav = page.locator('header nav');
+ await expect(mainNav).toBeVisible();
+ await expect(mainNav.locator('a[href="/docs"]')).toBeVisible();
+ });
+
+ test('should preserve layout when navigating between nested routes', async ({ page }) => {
+ // Start at docs index
+ await page.goto('/docs');
+
+ // Click on the Documentation link in the main nav
+ const mainNav = page.locator('header nav');
+ const docsLink = mainNav.locator('a[href="/docs"]');
+ await expect(docsLink).toBeVisible();
+ await expect(docsLink).toHaveAttribute('aria-current', 'page');
+
+ // Navigate to getting-started
+ await page.goto('/docs/getting-started');
+ await expect(page).toHaveURL('/docs/getting-started');
+
+ // The main navigation should still be visible
+ await expect(mainNav).toBeVisible();
+ await expect(docsLink).toBeVisible();
+
+ // Navigate to advanced
+ await page.goto('/docs/advanced');
+ await expect(page).toHaveURL('/docs/advanced');
+
+ // Navigation should still be preserved
+ await expect(mainNav).toBeVisible();
+ await expect(docsLink).toBeVisible();
+ });
+});
\ No newline at end of file
diff --git a/examples/custom-node-server/tests/e2e/home.test.ts b/examples/custom-node-server/tests/e2e/home.test.ts
new file mode 100644
index 0000000..4f3a679
--- /dev/null
+++ b/examples/custom-node-server/tests/e2e/home.test.ts
@@ -0,0 +1,52 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Home Page', () => {
+ test('should display welcome message and feature cards', async ({ page }) => {
+ // Navigate to home page
+ await page.goto('/');
+
+ // Check page title
+ await expect(page).toHaveTitle(/React Router Demo/);
+
+ // Check welcome message
+ const welcomeHeading = page.locator('h1:has-text("Welcome to React Router")');
+ await expect(welcomeHeading).toBeVisible();
+
+ // Check feature cards (there should be 3)
+ const featureCards = page.locator('.card h2').filter({
+ hasText: /Dynamic Routing|Nested Routes|Route Protection/
+ });
+ await expect(featureCards).toHaveCount(3);
+
+ // Test hover state on a feature card's parent
+ const firstFeatureCard = featureCards.first().locator('..').first();
+ await firstFeatureCard.hover();
+ await expect(firstFeatureCard).toHaveClass(/card.*cursor-pointer/);
+
+ // Test navigation to about page
+ const aboutPageLinks = page.locator('a[href="/about"]').filter({ hasText: 'View About Page' });
+ await expect(aboutPageLinks.first()).toBeVisible();
+ await aboutPageLinks.first().click();
+
+ // Verify navigation to about page
+ await expect(page).toHaveURL('/about');
+ await expect(page.locator('h1:has-text("About This Demo")')).toBeVisible();
+ });
+
+ test('should have working resource links', async ({ page }) => {
+ // Navigate to home page
+ await page.goto('/');
+
+ // Check resource cards
+ const resourceLinks = page.locator('a.card[target="_blank"]').filter({
+ hasText: /React Router Documentation|GitHub Repository|React Router Blog/
+ });
+ await expect(resourceLinks).toHaveCount(3);
+
+ // Test that links have proper attributes
+ for (const link of await resourceLinks.all()) {
+ await expect(link).toHaveAttribute('target', '_blank');
+ await expect(link).toHaveAttribute('rel', 'noreferrer');
+ }
+ });
+});
\ No newline at end of file
diff --git a/examples/custom-node-server/tests/e2e/navigation.test.ts b/examples/custom-node-server/tests/e2e/navigation.test.ts
new file mode 100644
index 0000000..62fe94e
--- /dev/null
+++ b/examples/custom-node-server/tests/e2e/navigation.test.ts
@@ -0,0 +1,26 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Navigation Flow', () => {
+ test('should navigate through all major sections of the app', async ({ page }) => {
+ // Start at the home page
+ await page.goto('/');
+ await expect(page).toHaveURL('/');
+
+ // Navigate to about page
+ await page.goto('/about');
+ await expect(page).toHaveURL('/about');
+
+ // Navigate to docs section
+ await page.goto('/docs');
+ await expect(page).toHaveURL('/docs');
+
+ // Navigate to projects section
+ await page.goto('/projects');
+ await expect(page).toHaveURL('/projects');
+
+ // Navigate to a specific project
+ const projectId = 'react-router';
+ await page.goto(`/projects/${projectId}`);
+ await expect(page).toHaveURL(`/projects/${projectId}`);
+ });
+});
\ No newline at end of file
diff --git a/examples/custom-node-server/tests/e2e/projects.test.ts b/examples/custom-node-server/tests/e2e/projects.test.ts
new file mode 100644
index 0000000..383ee81
--- /dev/null
+++ b/examples/custom-node-server/tests/e2e/projects.test.ts
@@ -0,0 +1,71 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Projects Section', () => {
+ test('should display projects listing', async ({ page }) => {
+ // Navigate to projects index
+ await page.goto('/projects');
+
+ // Verify the projects index page URL
+ await expect(page).toHaveURL('/projects');
+
+ // Verify the Projects link in nav is active
+ const projectsLink = page.locator('a.nav-link[href="/projects"]');
+ await expect(projectsLink).toBeVisible();
+ await expect(projectsLink).toHaveAttribute('aria-current', 'page');
+ });
+
+ test('should navigate to project detail page', async ({ page }) => {
+ const projectId = 'react-router';
+
+ // Go directly to the project page
+ await page.goto(`/projects/${projectId}`);
+
+ // Verify we're on the correct page
+ await expect(page).toHaveURL(`/projects/${projectId}`);
+
+ // Check project name is displayed
+ const projectName = page.locator('h1').first();
+ await expect(projectName).toBeVisible();
+
+ // Check edit and settings links in the navigation
+ const editLink = page.locator(`a[href="/projects/${projectId}/edit"]`);
+ await expect(editLink).toBeVisible();
+
+ const settingsLink = page.locator(`a[href="/projects/${projectId}/settings"]`);
+ await expect(settingsLink).toBeVisible();
+
+ // Check sections
+ const sections = page.locator('.card h2').filter({
+ hasText: /Progress|Team|Recent Activity/
+ });
+ await expect(sections).toHaveCount(3);
+ });
+
+ test('should navigate to project edit page', async ({ page }) => {
+ const projectId = 'react-router';
+
+ // Go to the project detail page
+ await page.goto(`/projects/${projectId}`);
+
+ // Click the edit link
+ const editLink = page.locator(`a[href="/projects/${projectId}/edit"]`);
+ await editLink.click();
+
+ // Verify we're on the edit page
+ await expect(page).toHaveURL(`/projects/${projectId}/edit`);
+ });
+
+ test('should navigate to project settings page', async ({ page }) => {
+ const projectId = 'react-router';
+
+ // Go to the project detail page
+ await page.goto(`/projects/${projectId}`);
+
+ // Click the settings link
+ const settingsLink = page.locator(`a[href="/projects/${projectId}/settings"]`);
+ await settingsLink.click();
+
+ // Verify we're on the settings page
+ await expect(page).toHaveURL(`/projects/${projectId}/settings`);
+ });
+});
\ No newline at end of file
diff --git a/examples/default-template/package.json b/examples/default-template/package.json
index 4baa616..750799d 100644
--- a/examples/default-template/package.json
+++ b/examples/default-template/package.json
@@ -23,14 +23,14 @@
"devDependencies": {
"@playwright/test": "^1.50.1",
"@react-router/dev": "^7.4.0",
- "@rsbuild/core": "^1.2.3",
- "@rsbuild/plugin-react": "^1.1.0",
+ "@rsbuild/core": "^1.2.19",
+ "@rsbuild/plugin-react": "^1.1.1",
"@rsbuild/plugin-react-router": "workspace:*",
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^20",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.1",
- "react-router-devtools": "^1.1.0",
+ "react-router-devtools": "^1.1.6",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.2",
"vite": "^5.4.11",
diff --git a/examples/epic-stack/app/routes/_auth+/login.tsx b/examples/epic-stack/app/routes/_auth+/login.tsx
index d012449..2528a51 100644
--- a/examples/epic-stack/app/routes/_auth+/login.tsx
+++ b/examples/epic-stack/app/routes/_auth+/login.tsx
@@ -4,10 +4,10 @@ import { type SEOHandle } from '@nasa-gcn/remix-seo'
import { data, Form, Link, useSearchParams } from 'react-router'
import { HoneypotInputs } from 'remix-utils/honeypot/react'
import { z } from 'zod'
-import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
-import { CheckboxField, ErrorList, Field } from '#app/components/forms.tsx'
-import { Spacer } from '#app/components/spacer.tsx'
-import { StatusButton } from '#app/components/ui/status-button.tsx'
+import { GeneralErrorBoundary } from '#app/components/error-boundary'
+import { CheckboxField, ErrorList, Field } from '#app/components/forms'
+import { Spacer } from '#app/components/spacer'
+import { StatusButton } from '#app/components/ui/status-button'
import { login, requireAnonymous } from '#app/utils/auth.server.ts'
import {
ProviderConnectionForm,
diff --git a/examples/epic-stack/package.json b/examples/epic-stack/package.json
index 04235ee..3d0d2c3 100644
--- a/examples/epic-stack/package.json
+++ b/examples/epic-stack/package.json
@@ -66,8 +66,8 @@
"@react-router/node": "^7.4.0",
"@react-router/remix-routes-option-adapter": "7.4.0",
"@remix-run/server-runtime": "2.15.3",
- "@rsbuild/core": "1.2.3",
- "@rsbuild/plugin-react": "1.1.0",
+ "@rsbuild/core": "1.2.19",
+ "@rsbuild/plugin-react": "1.1.1",
"@rsbuild/plugin-react-router": "workspace:*",
"@sentry/node": "8.54.0",
"@sentry/profiling-node": "8.54.0",
diff --git a/examples/federation/epic-stack-remote/.github/PULL_REQUEST_TEMPLATE.md b/examples/federation/epic-stack-remote/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..84a2084
--- /dev/null
+++ b/examples/federation/epic-stack-remote/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,15 @@
+
+
+## Test Plan
+
+
+
+## Checklist
+
+- [ ] Tests updated
+- [ ] Docs updated
+
+## Screenshots
+
+
diff --git a/examples/federation/epic-stack-remote/.github/workflows/deploy.yml b/examples/federation/epic-stack-remote/.github/workflows/deploy.yml
new file mode 100644
index 0000000..e0eac2b
--- /dev/null
+++ b/examples/federation/epic-stack-remote/.github/workflows/deploy.yml
@@ -0,0 +1,174 @@
+name: π Deploy
+on:
+ push:
+ branches:
+ - main
+ - dev
+ pull_request: {}
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ actions: write
+ contents: read
+
+jobs:
+ lint:
+ name: ⬣ ESLint
+ runs-on: ubuntu-22.04
+ steps:
+ - name: β¬οΈ Checkout repo
+ uses: actions/checkout@v4
+
+ - name: β Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: π₯ Download deps
+ uses: bahmutov/npm-install@v1
+
+ - name: πΌ Build icons
+ run: npm run build:icons
+
+ - name: π¬ Lint
+ run: npm run lint
+
+ typecheck:
+ name: Κ¦ TypeScript
+ runs-on: ubuntu-22.04
+ steps:
+ - name: β¬οΈ Checkout repo
+ uses: actions/checkout@v4
+
+ - name: β Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: π₯ Download deps
+ uses: bahmutov/npm-install@v1
+
+ - name: πΌ Build icons
+ run: npm run build:icons
+
+ - name: π Type check
+ run: npm run typecheck --if-present
+
+ vitest:
+ name: β‘ Vitest
+ runs-on: ubuntu-22.04
+ steps:
+ - name: β¬οΈ Checkout repo
+ uses: actions/checkout@v4
+
+ - name: β Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: π₯ Download deps
+ uses: bahmutov/npm-install@v1
+
+ - name: π Copy test env vars
+ run: cp .env.example .env
+
+ - name: πΌ Build icons
+ run: npm run build:icons
+
+ - name: β‘ Run vitest
+ run: npm run test -- --coverage
+
+ playwright:
+ name: π Playwright
+ runs-on: ubuntu-22.04
+ timeout-minutes: 60
+ steps:
+ - name: β¬οΈ Checkout repo
+ uses: actions/checkout@v4
+
+ - name: π Copy test env vars
+ run: cp .env.example .env
+
+ - name: β Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: π₯ Download deps
+ uses: bahmutov/npm-install@v1
+
+ - name: π₯ Install Playwright Browsers
+ run: npm run test:e2e:install
+
+ - name: π Setup Database
+ run: npx prisma migrate deploy
+
+ - name: π¦ Cache Database
+ id: db-cache
+ uses: actions/cache@v4
+ with:
+ path: prisma/data.db
+ key:
+ db-cache-schema_${{ hashFiles('./prisma/schema.prisma')
+ }}-migrations_${{ hashFiles('./prisma/migrations/*/migration.sql')
+ }}
+
+ - name: π± Seed Database
+ if: steps.db-cache.outputs.cache-hit != 'true'
+ run: npx prisma migrate reset --force
+
+ - name: π Build
+ run: npm run build
+
+ - name: π Playwright tests
+ run: npx playwright test
+
+ - name: π Upload report
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 30
+
+ deploy:
+ name: π Deploy
+ runs-on: ubuntu-22.04
+ needs: [lint, typecheck, vitest, playwright]
+ # only build/deploy branches on pushes
+ if: ${{ github.event_name == 'push' }}
+
+ steps:
+ - name: β¬οΈ Checkout repo
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: '50'
+
+ - name: π Read app name
+ uses: SebRollen/toml-action@v1.2.0
+ id: app_name
+ with:
+ file: 'fly.toml'
+ field: 'app'
+
+ - name: π Setup Fly
+ uses: superfly/flyctl-actions/setup-flyctl@1.5
+
+ - name: π Deploy Staging
+ if: ${{ github.ref == 'refs/heads/dev' }}
+ run:
+ flyctl deploy --remote-only --build-arg COMMIT_SHA=${{ github.sha }}
+ --app ${{ steps.app_name.outputs.value }}-staging
+ env:
+ FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
+
+ - name: π Deploy Production
+ if: ${{ github.ref == 'refs/heads/main' }}
+ run:
+ flyctl deploy --remote-only --build-arg COMMIT_SHA=${{ github.sha }}
+ --build-secret SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
+ env:
+ FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
diff --git a/examples/federation/epic-stack-remote/.github/workflows/version.yml b/examples/federation/epic-stack-remote/.github/workflows/version.yml
new file mode 100644
index 0000000..1a7515d
--- /dev/null
+++ b/examples/federation/epic-stack-remote/.github/workflows/version.yml
@@ -0,0 +1,54 @@
+name: π Version
+on:
+ push:
+ branches:
+ - main
+ - dev
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: write
+
+jobs:
+ version:
+ name: π Update Version
+ runs-on: ubuntu-22.04
+ if: ${{ github.event_name == 'push' }}
+
+ steps:
+ - name: β¬οΈ Checkout repo
+ uses: actions/checkout@v4
+
+ - name: π’ Get HEAD commit hash
+ id: get_head_hash
+ run: echo "HEAD_HASH=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
+
+ - name: π
Get current date
+ id: get_date
+ run:
+ echo "CURRENT_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
+
+ - name: π Update package.json
+ run: |
+ jq '
+ if .["epic-stack"] then
+ .["epic-stack"].head = "${{ steps.get_head_hash.outputs.HEAD_HASH }}" |
+ .["epic-stack"].date = "${{ steps.get_date.outputs.CURRENT_DATE }}"
+ else
+ .["epic-stack"] = {
+ "head": "${{ steps.get_head_hash.outputs.HEAD_HASH }}",
+ "date": "${{ steps.get_date.outputs.CURRENT_DATE }}"
+ }
+ end
+ ' package.json > temp.json && mv temp.json package.json
+
+ - name: πΎ Commit changes
+ run: |
+ git config --local user.email "kody@epicweb.dev"
+ git config --local user.name "kody"
+ git add package.json
+ git commit -m "Update epic-stack version [skip ci]"
+ git push
diff --git a/examples/federation/epic-stack-remote/.gitignore b/examples/federation/epic-stack-remote/.gitignore
new file mode 100644
index 0000000..6ba5a4f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/.gitignore
@@ -0,0 +1,26 @@
+node_modules
+.DS_store
+
+/build
+/server-build
+.env
+.cache
+
+/prisma/data.db
+/prisma/data.db-journal
+/tests/prisma
+
+/test-results/
+/playwright-report/
+/playwright/.cache/
+/tests/fixtures/email/
+/coverage
+
+/other/cache.db
+
+# Easy way to create temporary files/folders that won't accidentally be added to git
+*.local.*
+
+# generated files
+/app/components/ui/icons
+.react-router/
diff --git a/examples/federation/epic-stack-remote/.prettierignore b/examples/federation/epic-stack-remote/.prettierignore
new file mode 100644
index 0000000..f022d02
--- /dev/null
+++ b/examples/federation/epic-stack-remote/.prettierignore
@@ -0,0 +1,15 @@
+node_modules
+
+/build
+/public/build
+/server-build
+.env
+
+/test-results/
+/playwright-report/
+/playwright/.cache/
+/tests/fixtures/email/*.json
+/coverage
+/prisma/migrations
+
+package-lock.json
diff --git a/examples/federation/epic-stack-remote/.vscode/extensions.json b/examples/federation/epic-stack-remote/.vscode/extensions.json
new file mode 100644
index 0000000..3c0a690
--- /dev/null
+++ b/examples/federation/epic-stack-remote/.vscode/extensions.json
@@ -0,0 +1,11 @@
+{
+ "recommendations": [
+ "bradlc.vscode-tailwindcss",
+ "dbaeumer.vscode-eslint",
+ "esbenp.prettier-vscode",
+ "prisma.prisma",
+ "qwtel.sqlite-viewer",
+ "yoavbls.pretty-ts-errors",
+ "github.vscode-github-actions"
+ ]
+}
diff --git a/examples/federation/epic-stack-remote/.vscode/remix.code-snippets b/examples/federation/epic-stack-remote/.vscode/remix.code-snippets
new file mode 100644
index 0000000..39e959f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/.vscode/remix.code-snippets
@@ -0,0 +1,80 @@
+{
+ "loader": {
+ "prefix": "/loader",
+ "scope": "typescriptreact,javascriptreact,typescript,javascript",
+ "body": [
+ "import { type Route } from \"./+types/${TM_FILENAME_BASE}.ts\"",
+ "",
+ "export async function loader({ request }: Route.LoaderArgs) {",
+ " return {}",
+ "}",
+ ],
+ },
+ "action": {
+ "prefix": "/action",
+ "scope": "typescriptreact,javascriptreact,typescript,javascript",
+ "body": [
+ "import { type Route } from \"./+types/${TM_FILENAME_BASE}.ts\"",
+ "",
+ "export async function action({ request }: Route.ActionArgs) {",
+ " return {}",
+ "}",
+ ],
+ },
+ "default": {
+ "prefix": "/default",
+ "scope": "typescriptreact,javascriptreact,typescript,javascript",
+ "body": [
+ "export default function ${TM_FILENAME_BASE/[^a-zA-Z0-9]*([a-zA-Z0-9])([a-zA-Z0-9]*)/${1:/capitalize}${2}/g}() {",
+ " return (",
+ "
",
+ "
Unknown Route ",
+ " ",
+ " )",
+ "}",
+ ],
+ },
+ "headers": {
+ "prefix": "/headers",
+ "scope": "typescriptreact,javascriptreact,typescript,javascript",
+ "body": [
+ "import { type Route } from \"./+types/${TM_FILENAME_BASE}.ts\"",
+ "export const headers: Route.HeadersFunction = ({ loaderHeaders }) => ({",
+ " 'Cache-Control': loaderHeaders.get('Cache-Control') ?? '',",
+ "})",
+ ],
+ },
+ "links": {
+ "prefix": "/links",
+ "scope": "typescriptreact,javascriptreact,typescript,javascript",
+ "body": [
+ "import { type Route } from \"./+types/${TM_FILENAME_BASE}.ts\"",
+ "",
+ "export const links: Route.LinksFunction = () => {",
+ " return []",
+ "}",
+ ],
+ },
+ "meta": {
+ "prefix": "/meta",
+ "scope": "typescriptreact,javascriptreact,typescript,javascript",
+ "body": [
+ "import { type Route } from \"./+types/${TM_FILENAME_BASE}.ts\"",
+ "",
+ "export const meta: Route.MetaFunction = ({ data }) => [{",
+ " title: 'Title',",
+ "}]",
+ ],
+ },
+ "shouldRevalidate": {
+ "prefix": "/shouldRevalidate",
+ "scope": "typescriptreact,javascriptreact,typescript,javascript",
+ "body": [
+ "import { type ShouldRevalidateFunctionArgs } from 'react-router'",
+ "",
+ "export function shouldRevalidate({ defaultShouldRevalidate }: ShouldRevalidateFunctionArgs) {",
+ " return defaultShouldRevalidate",
+ "}",
+ ],
+ },
+}
\ No newline at end of file
diff --git a/examples/federation/epic-stack-remote/.vscode/settings.json b/examples/federation/epic-stack-remote/.vscode/settings.json
new file mode 100644
index 0000000..9ec5cad
--- /dev/null
+++ b/examples/federation/epic-stack-remote/.vscode/settings.json
@@ -0,0 +1,14 @@
+{
+ "typescript.preferences.autoImportFileExcludePatterns": [
+ "@remix-run/server-runtime",
+ "express",
+ "@radix-ui/**",
+ "@react-email/**",
+ "node:stream/consumers",
+ "node:test",
+ "node:console"
+ ],
+ "workbench.editorAssociations": {
+ "*.db": "sqlite-viewer.view"
+ }
+}
diff --git a/examples/federation/epic-stack-remote/CONTRIBUTING.md b/examples/federation/epic-stack-remote/CONTRIBUTING.md
new file mode 100644
index 0000000..2a1e49f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/CONTRIBUTING.md
@@ -0,0 +1,87 @@
+# Contributing
+
+Thanks for your willingness to contribute! Please make sure to check with me
+before doing a bunch of work on something.
+
+## Project setup
+
+If you do need to set the project up locally yourself, feel free to follow these
+instructions:
+
+### System Requirements
+
+- [Node.js](https://nodejs.org/) >= 20.0.0
+- [npm](https://npmjs.com/) >= 8.18.0
+- [git](https://git-scm.com/) >= 2.38.0
+
+### Setup steps
+
+1. Fork repo
+2. clone the repo
+3. Copy `.env.example` into `.env`
+4. Run `npm install && npm run setup -s` to install dependencies and run
+ validation
+5. Create a branch for your PR with `git checkout -b pr/your-branch-name`
+
+> Tip: Keep your `main` branch pointing at the original repository and make pull
+> requests from branches on your fork. To do this, run:
+>
+> ```
+> git remote add upstream https://github.com/epicweb-dev/epic-stack.git
+> git fetch upstream
+> git branch --set-upstream-to=upstream/main main
+> ```
+>
+> This will add the original repository as a "remote" called "upstream," Then
+> fetch the git information from that remote, then set your local `main` branch
+> to use the upstream main branch whenever you run `git pull`. Then you can make
+> all of your pull request branches based on this `main` branch. Whenever you
+> want to update your version of `main`, do a regular `git pull`.
+
+If the setup script doesn't work, you can try to run the commands manually:
+
+```sh
+git clone
+cd ./epic-stack
+
+# copy the .env.example to .env
+# everything's mocked out during development so you shouldn't need to
+# change any of these values unless you want to hit real environments.
+cp .env.example .env
+
+# Install deps
+npm install
+
+# setup database
+prisma migrate reset --force
+
+# Install playwright browsers
+npm run test:e2e:install
+
+# run build, typecheck, linting
+npm run validate
+```
+
+If that all worked without trouble, you should be able to start development
+with:
+
+```sh
+npm run dev
+```
+
+And open up `http://localhost:3000` and rock!
+
+## Help Needed
+
+There's something to be said for custom code and the ability that grants with
+regard to tuning it to be exactly what you need. But there's also something to
+be said for offloading maintenance onto external dependencies. There are likely
+several bits of code in this codebase that could benefit from externalization.
+There could even be some things that could be improved by existing libraries.
+
+Feel free to take any code from within this project and turn it into an open
+source library (appropriate attribution is appreciated). Then come back and make
+a PR to use your new library.
+
+NOTE: Actual adoption of your library is not guaranteed. Offloading maintenance
+and adaptability is a delicate balance.
diff --git a/examples/federation/epic-stack-remote/LICENSE.md b/examples/federation/epic-stack-remote/LICENSE.md
new file mode 100644
index 0000000..7c76ba3
--- /dev/null
+++ b/examples/federation/epic-stack-remote/LICENSE.md
@@ -0,0 +1,18 @@
+Copyright Β© 2023 Kent C. Dodds
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the βSoftwareβ), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED βAS ISβ, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/examples/federation/epic-stack-remote/README.md b/examples/federation/epic-stack-remote/README.md
new file mode 100644
index 0000000..cc8f52f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/README.md
@@ -0,0 +1,54 @@
+
+
+
+ Ditch analysis paralysis and start shipping Epic Web apps.
+
+
+ This is an opinionated project starter and reference that allows teams to
+ ship their ideas to production faster and on a more stable foundation based
+ on the experience of Kent C. Dodds and
+ contributors .
+
+
+
+```sh
+npx create-epic-app@latest
+```
+
+[](https://www.epicweb.dev/epic-stack)
+
+[The Epic Stack](https://www.epicweb.dev/epic-stack)
+
+
+
+## Watch Kent's Introduction to The Epic Stack
+
+[](https://www.epicweb.dev/talks/the-epic-stack)
+
+["The Epic Stack" by Kent C. Dodds](https://www.epicweb.dev/talks/the-epic-stack)
+
+## Docs
+
+[Read the docs](https://github.com/epicweb-dev/epic-stack/blob/main/docs)
+(please π).
+
+## Support
+
+- π Join the
+ [discussion on GitHub](https://github.com/epicweb-dev/epic-stack/discussions)
+ and the [KCD Community on Discord](https://kcd.im/discord).
+- π‘ Create an
+ [idea discussion](https://github.com/epicweb-dev/epic-stack/discussions/new?category=ideas)
+ for suggestions.
+- π Open a [GitHub issue](https://github.com/epicweb-dev/epic-stack/issues) to
+ report a bug.
+
+## Branding
+
+Want to talk about the Epic Stack in a blog post or talk? Great! Here are some
+assets you can use in your material:
+[EpicWeb.dev/brand](https://epicweb.dev/brand)
+
+## Thanks
+
+You rock πͺ¨
diff --git a/examples/federation/epic-stack-remote/app/assets/favicons/apple-touch-icon.png b/examples/federation/epic-stack-remote/app/assets/favicons/apple-touch-icon.png
new file mode 100644
index 0000000..8bf4632
Binary files /dev/null and b/examples/federation/epic-stack-remote/app/assets/favicons/apple-touch-icon.png differ
diff --git a/examples/federation/epic-stack-remote/app/assets/favicons/favicon.svg b/examples/federation/epic-stack-remote/app/assets/favicons/favicon.svg
new file mode 100644
index 0000000..72be6f0
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/assets/favicons/favicon.svg
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/app/components/error-boundary.tsx b/examples/federation/epic-stack-remote/app/components/error-boundary.tsx
new file mode 100644
index 0000000..0aad131
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/error-boundary.tsx
@@ -0,0 +1,53 @@
+import { captureException } from '@sentry/react'
+import { useEffect, type ReactElement } from 'react'
+import {
+ type ErrorResponse,
+ isRouteErrorResponse,
+ useParams,
+ useRouteError,
+} from 'react-router'
+import { getErrorMessage } from '#app/utils/misc'
+
+type StatusHandler = (info: {
+ error: ErrorResponse
+ params: Record
+}) => ReactElement | null
+
+export function GeneralErrorBoundary({
+ defaultStatusHandler = ({ error }) => (
+
+ {error.status} {error.data}
+
+ ),
+ statusHandlers,
+ unexpectedErrorHandler = (error) => {getErrorMessage(error)}
,
+}: {
+ defaultStatusHandler?: StatusHandler
+ statusHandlers?: Record
+ unexpectedErrorHandler?: (error: unknown) => ReactElement | null
+}) {
+ const error = useRouteError()
+ const params = useParams()
+ const isResponse = isRouteErrorResponse(error)
+
+ if (typeof document !== 'undefined') {
+ console.error(error)
+ }
+
+ useEffect(() => {
+ if (isResponse) return
+
+ captureException(error)
+ }, [error, isResponse])
+
+ return (
+
+ {isResponse
+ ? (statusHandlers?.[error.status] ?? defaultStatusHandler)({
+ error,
+ params,
+ })
+ : unexpectedErrorHandler(error)}
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/components/floating-toolbar.tsx b/examples/federation/epic-stack-remote/app/components/floating-toolbar.tsx
new file mode 100644
index 0000000..41b5be0
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/floating-toolbar.tsx
@@ -0,0 +1,2 @@
+export const floatingToolbarClassName =
+ 'absolute bottom-3 left-3 right-3 flex items-center gap-2 rounded-lg bg-muted/80 p-4 pl-5 shadow-xl shadow-accent backdrop-blur-sm md:gap-4 md:pl-7 justify-end'
diff --git a/examples/federation/epic-stack-remote/app/components/forms.tsx b/examples/federation/epic-stack-remote/app/components/forms.tsx
new file mode 100644
index 0000000..9d3644a
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/forms.tsx
@@ -0,0 +1,202 @@
+import { useInputControl } from '@conform-to/react'
+import { REGEXP_ONLY_DIGITS_AND_CHARS, type OTPInputProps } from 'input-otp'
+import React, { useId } from 'react'
+import { Checkbox, type CheckboxProps } from './ui/checkbox.tsx'
+import {
+ InputOTP,
+ InputOTPGroup,
+ InputOTPSeparator,
+ InputOTPSlot,
+} from './ui/input-otp.tsx'
+import { Input } from './ui/input.tsx'
+import { Label } from './ui/label.tsx'
+import { Textarea } from './ui/textarea.tsx'
+
+export type ListOfErrors = Array | null | undefined
+
+export function ErrorList({
+ id,
+ errors,
+}: {
+ errors?: ListOfErrors
+ id?: string
+}) {
+ const errorsToRender = errors?.filter(Boolean)
+ if (!errorsToRender?.length) return null
+ return (
+
+ {errorsToRender.map((e) => (
+
+ {e}
+
+ ))}
+
+ )
+}
+
+export function Field({
+ labelProps,
+ inputProps,
+ errors,
+ className,
+}: {
+ labelProps: React.LabelHTMLAttributes
+ inputProps: React.InputHTMLAttributes
+ errors?: ListOfErrors
+ className?: string
+}) {
+ const fallbackId = useId()
+ const id = inputProps.id ?? fallbackId
+ const errorId = errors?.length ? `${id}-error` : undefined
+ return (
+
+
+
+
+ {errorId ? : null}
+
+
+ )
+}
+
+export function OTPField({
+ labelProps,
+ inputProps,
+ errors,
+ className,
+}: {
+ labelProps: React.LabelHTMLAttributes
+ inputProps: Partial
+ errors?: ListOfErrors
+ className?: string
+}) {
+ const fallbackId = useId()
+ const id = inputProps.id ?? fallbackId
+ const errorId = errors?.length ? `${id}-error` : undefined
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {errorId ? : null}
+
+
+ )
+}
+
+export function TextareaField({
+ labelProps,
+ textareaProps,
+ errors,
+ className,
+}: {
+ labelProps: React.LabelHTMLAttributes
+ textareaProps: React.TextareaHTMLAttributes
+ errors?: ListOfErrors
+ className?: string
+}) {
+ const fallbackId = useId()
+ const id = textareaProps.id ?? textareaProps.name ?? fallbackId
+ const errorId = errors?.length ? `${id}-error` : undefined
+ return (
+
+
+
+
+ {errorId ? : null}
+
+
+ )
+}
+
+export function CheckboxField({
+ labelProps,
+ buttonProps,
+ errors,
+ className,
+}: {
+ labelProps: React.ComponentProps<'label'>
+ buttonProps: CheckboxProps & {
+ name: string
+ form: string
+ value?: string
+ }
+ errors?: ListOfErrors
+ className?: string
+}) {
+ const { key, defaultChecked, ...checkboxProps } = buttonProps
+ const fallbackId = useId()
+ const checkedValue = buttonProps.value ?? 'on'
+ const input = useInputControl({
+ key,
+ name: buttonProps.name,
+ formId: buttonProps.form,
+ initialValue: defaultChecked ? checkedValue : undefined,
+ })
+ const id = buttonProps.id ?? fallbackId
+ const errorId = errors?.length ? `${id}-error` : undefined
+
+ return (
+
+
+ {
+ input.change(state.valueOf() ? checkedValue : '')
+ buttonProps.onCheckedChange?.(state)
+ }}
+ onFocus={(event) => {
+ input.focus()
+ buttonProps.onFocus?.(event)
+ }}
+ onBlur={(event) => {
+ input.blur()
+ buttonProps.onBlur?.(event)
+ }}
+ type="button"
+ />
+
+
+
+ {errorId ? : null}
+
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/components/progress-bar.tsx b/examples/federation/epic-stack-remote/app/components/progress-bar.tsx
new file mode 100644
index 0000000..d493f70
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/progress-bar.tsx
@@ -0,0 +1,63 @@
+import { useEffect, useRef, useState } from 'react'
+import { useNavigation } from 'react-router'
+import { useSpinDelay } from 'spin-delay'
+import { cn } from '#app/utils/misc.tsx'
+import { Icon } from './ui/icon.tsx'
+
+function EpicProgress() {
+ const transition = useNavigation()
+ const busy = transition.state !== 'idle'
+ const delayedPending = useSpinDelay(busy, {
+ delay: 600,
+ minDuration: 400,
+ })
+ const ref = useRef(null)
+ const [animationComplete, setAnimationComplete] = useState(true)
+
+ useEffect(() => {
+ if (!ref.current) return
+ if (delayedPending) setAnimationComplete(false)
+
+ const animationPromises = ref.current
+ .getAnimations()
+ .map(({ finished }) => finished)
+
+ void Promise.allSettled(animationPromises).then(() => {
+ if (!delayedPending) setAnimationComplete(true)
+ })
+ }, [delayedPending])
+
+ return (
+
+
+ {delayedPending && (
+
+
+
+ )}
+
+ )
+}
+
+export { EpicProgress }
diff --git a/examples/federation/epic-stack-remote/app/components/search-bar.tsx b/examples/federation/epic-stack-remote/app/components/search-bar.tsx
new file mode 100644
index 0000000..db7af73
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/search-bar.tsx
@@ -0,0 +1,63 @@
+import { useId } from 'react'
+import { Form, useSearchParams, useSubmit } from 'react-router'
+import { useDebounce, useIsPending } from '#app/utils/misc.tsx'
+import { Icon } from './ui/icon.tsx'
+import { Input } from './ui/input.tsx'
+import { Label } from './ui/label.tsx'
+import { StatusButton } from './ui/status-button.tsx'
+
+export function SearchBar({
+ status,
+ autoFocus = false,
+ autoSubmit = false,
+}: {
+ status: 'idle' | 'pending' | 'success' | 'error'
+ autoFocus?: boolean
+ autoSubmit?: boolean
+}) {
+ const id = useId()
+ const [searchParams] = useSearchParams()
+ const submit = useSubmit()
+ const isSubmitting = useIsPending({
+ formMethod: 'GET',
+ formAction: '/users',
+ })
+
+ const handleFormChange = useDebounce(async (form: HTMLFormElement) => {
+ await submit(form)
+ }, 400)
+
+ return (
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/components/spacer.tsx b/examples/federation/epic-stack-remote/app/components/spacer.tsx
new file mode 100644
index 0000000..8a8e537
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/spacer.tsx
@@ -0,0 +1,57 @@
+export function Spacer({
+ size,
+}: {
+ /**
+ * The size of the space
+ *
+ * 4xs: h-4 (16px)
+ *
+ * 3xs: h-8 (32px)
+ *
+ * 2xs: h-12 (48px)
+ *
+ * xs: h-16 (64px)
+ *
+ * sm: h-20 (80px)
+ *
+ * md: h-24 (96px)
+ *
+ * lg: h-28 (112px)
+ *
+ * xl: h-32 (128px)
+ *
+ * 2xl: h-36 (144px)
+ *
+ * 3xl: h-40 (160px)
+ *
+ * 4xl: h-44 (176px)
+ */
+ size:
+ | '4xs'
+ | '3xs'
+ | '2xs'
+ | 'xs'
+ | 'sm'
+ | 'md'
+ | 'lg'
+ | 'xl'
+ | '2xl'
+ | '3xl'
+ | '4xl'
+}) {
+ const options: Record = {
+ '4xs': 'h-4',
+ '3xs': 'h-8',
+ '2xs': 'h-12',
+ xs: 'h-16',
+ sm: 'h-20',
+ md: 'h-24',
+ lg: 'h-28',
+ xl: 'h-32',
+ '2xl': 'h-36',
+ '3xl': 'h-40',
+ '4xl': 'h-44',
+ }
+ const className = options[size]
+ return
+}
diff --git a/examples/federation/epic-stack-remote/app/components/toaster.tsx b/examples/federation/epic-stack-remote/app/components/toaster.tsx
new file mode 100644
index 0000000..954b2db
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/toaster.tsx
@@ -0,0 +1,16 @@
+import { useEffect } from 'react'
+import { toast as showToast } from 'sonner'
+import { type Toast } from '#app/utils/toast.server.ts'
+
+export function useToast(toast?: Toast | null) {
+ useEffect(() => {
+ if (toast) {
+ setTimeout(() => {
+ showToast[toast.type](toast.title, {
+ id: toast.id,
+ description: toast.description,
+ })
+ }, 0)
+ }
+ }, [toast])
+}
diff --git a/examples/federation/epic-stack-remote/app/components/ui/README.md b/examples/federation/epic-stack-remote/app/components/ui/README.md
new file mode 100644
index 0000000..433847d
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/ui/README.md
@@ -0,0 +1,7 @@
+# shadcn/ui
+
+Some components in this directory are downloaded via the
+[shadcn/ui](https://ui.shadcn.com) [CLI](https://ui.shadcn.com/docs/cli). Feel
+free to customize them to your needs. It's important to know that shadcn/ui is
+not a library of components you install, but instead it's a registry of prebuilt
+components which you can download and customize.
diff --git a/examples/federation/epic-stack-remote/app/components/ui/button.tsx b/examples/federation/epic-stack-remote/app/components/ui/button.tsx
new file mode 100644
index 0000000..36601c5
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/ui/button.tsx
@@ -0,0 +1,58 @@
+import { Slot } from '@radix-ui/react-slot'
+import { cva, type VariantProps } from 'class-variance-authority'
+import * as React from 'react'
+
+import { cn } from '#app/utils/misc.tsx'
+
+const buttonVariants = cva(
+ 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors outline-none focus-visible:ring-2 focus-within:ring-2 ring-ring ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground hover:bg-primary/80',
+ destructive:
+ 'bg-destructive text-destructive-foreground hover:bg-destructive/80',
+ outline:
+ 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
+ secondary:
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ size: {
+ default: 'h-10 px-4 py-2',
+ wide: 'px-24 py-5',
+ sm: 'h-9 rounded-md px-3',
+ lg: 'h-11 rounded-md px-8',
+ pill: 'px-12 py-3 leading-3',
+ icon: 'h-10 w-10',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'button'
+ return (
+
+ )
+ },
+)
+Button.displayName = 'Button'
+
+export { Button, buttonVariants }
diff --git a/examples/federation/epic-stack-remote/app/components/ui/checkbox.tsx b/examples/federation/epic-stack-remote/app/components/ui/checkbox.tsx
new file mode 100644
index 0000000..637a7fd
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/ui/checkbox.tsx
@@ -0,0 +1,41 @@
+import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
+import * as React from 'react'
+
+import { cn } from '#app/utils/misc.tsx'
+
+export type CheckboxProps = Omit<
+ React.ComponentPropsWithoutRef,
+ 'type'
+> & {
+ type?: string
+}
+
+const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+
+
+))
+Checkbox.displayName = CheckboxPrimitive.Root.displayName
+
+export { Checkbox }
diff --git a/examples/federation/epic-stack-remote/app/components/ui/dropdown-menu.tsx b/examples/federation/epic-stack-remote/app/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..3bb4fe3
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/ui/dropdown-menu.tsx
@@ -0,0 +1,206 @@
+import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
+import * as React from 'react'
+
+import { cn } from '#app/utils/misc.tsx'
+
+const DropdownMenu = DropdownMenuPrimitive.Root
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+ βΆοΈ
+
+))
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ βͺ
+
+
+ {children}
+
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+}
diff --git a/examples/federation/epic-stack-remote/app/components/ui/icon.tsx b/examples/federation/epic-stack-remote/app/components/ui/icon.tsx
new file mode 100644
index 0000000..10b8c1d
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/ui/icon.tsx
@@ -0,0 +1,77 @@
+import { type SVGProps } from 'react'
+import { cn } from '#app/utils/misc.tsx'
+import href from './icons/sprite.svg'
+import { type IconName } from '@/icon-name'
+
+export { href }
+export { IconName }
+
+const sizeClassName = {
+ font: 'w-[1em] h-[1em]',
+ xs: 'w-3 h-3',
+ sm: 'w-4 h-4',
+ md: 'w-5 h-5',
+ lg: 'w-6 h-6',
+ xl: 'w-7 h-7',
+} as const
+
+type Size = keyof typeof sizeClassName
+
+const childrenSizeClassName = {
+ font: 'gap-1.5',
+ xs: 'gap-1.5',
+ sm: 'gap-1.5',
+ md: 'gap-2',
+ lg: 'gap-2',
+ xl: 'gap-3',
+} satisfies Record
+
+/**
+ * Renders an SVG icon. The icon defaults to the size of the font. To make it
+ * align vertically with neighboring text, you can pass the text as a child of
+ * the icon and it will be automatically aligned.
+ * Alternatively, if you're not ok with the icon being to the left of the text,
+ * you need to wrap the icon and text in a common parent and set the parent to
+ * display "flex" (or "inline-flex") with "items-center" and a reasonable gap.
+ *
+ * Pass `title` prop to the `Icon` component to get `` element rendered
+ * in the SVG container, providing this way for accessibility.
+ */
+export function Icon({
+ name,
+ size = 'font',
+ className,
+ title,
+ children,
+ ...props
+}: SVGProps & {
+ name: IconName
+ size?: Size
+ title?: string
+}) {
+ if (children) {
+ return (
+
+
+ {children}
+
+ )
+ }
+ return (
+
+ {title ? {title} : null}
+
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/components/ui/input-otp.tsx b/examples/federation/epic-stack-remote/app/components/ui/input-otp.tsx
new file mode 100644
index 0000000..ff7a2d7
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/ui/input-otp.tsx
@@ -0,0 +1,70 @@
+import { OTPInput, OTPInputContext } from 'input-otp'
+import * as React from 'react'
+
+import { cn } from '#app/utils/misc.tsx'
+
+const InputOTP = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, containerClassName, ...props }, ref) => (
+
+))
+InputOTP.displayName = 'InputOTP'
+
+const InputOTPGroup = React.forwardRef<
+ React.ElementRef<'div'>,
+ React.ComponentPropsWithoutRef<'div'>
+>(({ className, ...props }, ref) => (
+
+))
+InputOTPGroup.displayName = 'InputOTPGroup'
+
+const InputOTPSlot = React.forwardRef<
+ React.ElementRef<'div'>,
+ React.ComponentPropsWithoutRef<'div'> & { index: number }
+>(({ index, className, ...props }, ref) => {
+ const inputOTPContext = React.useContext(OTPInputContext)
+ const slot = inputOTPContext.slots[index]
+ if (!slot) throw new Error('Invalid slot index')
+ const { char, hasFakeCaret, isActive } = slot
+
+ return (
+
+ {char}
+ {hasFakeCaret && (
+
+ )}
+
+ )
+})
+InputOTPSlot.displayName = 'InputOTPSlot'
+
+const InputOTPSeparator = React.forwardRef<
+ React.ElementRef<'div'>,
+ React.ComponentPropsWithoutRef<'div'>
+>(({ ...props }, ref) => (
+
+ -
+
+))
+InputOTPSeparator.displayName = 'InputOTPSeparator'
+
+export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
diff --git a/examples/federation/epic-stack-remote/app/components/ui/input.tsx b/examples/federation/epic-stack-remote/app/components/ui/input.tsx
new file mode 100644
index 0000000..f4b2db3
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react'
+
+import { cn } from '#app/utils/misc.tsx'
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ },
+)
+Input.displayName = 'Input'
+
+export { Input }
diff --git a/examples/federation/epic-stack-remote/app/components/ui/label.tsx b/examples/federation/epic-stack-remote/app/components/ui/label.tsx
new file mode 100644
index 0000000..ec453ee
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/ui/label.tsx
@@ -0,0 +1,24 @@
+import * as LabelPrimitive from '@radix-ui/react-label'
+import { cva, type VariantProps } from 'class-variance-authority'
+import * as React from 'react'
+
+import { cn } from '#app/utils/misc.tsx'
+
+const labelVariants = cva(
+ 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/examples/federation/epic-stack-remote/app/components/ui/sonner.tsx b/examples/federation/epic-stack-remote/app/components/ui/sonner.tsx
new file mode 100644
index 0000000..029a2a4
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/ui/sonner.tsx
@@ -0,0 +1,26 @@
+import { Toaster as Sonner } from 'sonner'
+
+type ToasterProps = React.ComponentProps
+
+const EpicToaster = ({ theme, ...props }: ToasterProps) => {
+ return (
+
+ )
+}
+
+export { EpicToaster }
diff --git a/examples/federation/epic-stack-remote/app/components/ui/status-button.tsx b/examples/federation/epic-stack-remote/app/components/ui/status-button.tsx
new file mode 100644
index 0000000..f39a7ff
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/ui/status-button.tsx
@@ -0,0 +1,78 @@
+import * as React from 'react'
+import { useSpinDelay } from 'spin-delay'
+import { cn } from '#app/utils/misc.tsx'
+import { Button, type ButtonProps } from './button.tsx'
+import { Icon } from './icon.tsx'
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from './tooltip.tsx'
+
+export const StatusButton = React.forwardRef<
+ HTMLButtonElement,
+ ButtonProps & {
+ status: 'pending' | 'success' | 'error' | 'idle'
+ message?: string | null
+ spinDelay?: Parameters[1]
+ }
+>(({ message, status, className, children, spinDelay, ...props }, ref) => {
+ const delayedPending = useSpinDelay(status === 'pending', {
+ delay: 400,
+ minDuration: 300,
+ ...spinDelay,
+ })
+ const companion = {
+ pending: delayedPending ? (
+
+
+
+ ) : null,
+ success: (
+
+
+
+ ),
+ error: (
+
+
+
+ ),
+ idle: null,
+ }[status]
+
+ return (
+
+ {children}
+ {message ? (
+
+
+ {companion}
+ {message}
+
+
+ ) : (
+ companion
+ )}
+
+ )
+})
+StatusButton.displayName = 'Button'
diff --git a/examples/federation/epic-stack-remote/app/components/ui/textarea.tsx b/examples/federation/epic-stack-remote/app/components/ui/textarea.tsx
new file mode 100644
index 0000000..2e8ea5e
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/ui/textarea.tsx
@@ -0,0 +1,24 @@
+import * as React from 'react'
+
+import { cn } from '#app/utils/misc.tsx'
+
+export interface TextareaProps
+ extends React.TextareaHTMLAttributes {}
+
+const Textarea = React.forwardRef(
+ ({ className, ...props }, ref) => {
+ return (
+
+ )
+ },
+)
+Textarea.displayName = 'Textarea'
+
+export { Textarea }
diff --git a/examples/federation/epic-stack-remote/app/components/ui/tooltip.tsx b/examples/federation/epic-stack-remote/app/components/ui/tooltip.tsx
new file mode 100644
index 0000000..5017f3e
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/ui/tooltip.tsx
@@ -0,0 +1,28 @@
+import * as TooltipPrimitive from '@radix-ui/react-tooltip'
+import * as React from 'react'
+
+import { cn } from '#app/utils/misc.tsx'
+
+const TooltipProvider = TooltipPrimitive.Provider
+
+const Tooltip = TooltipPrimitive.Root
+
+const TooltipTrigger = TooltipPrimitive.Trigger
+
+const TooltipContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+))
+TooltipContent.displayName = TooltipPrimitive.Content.displayName
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/examples/federation/epic-stack-remote/app/components/user-dropdown.tsx b/examples/federation/epic-stack-remote/app/components/user-dropdown.tsx
new file mode 100644
index 0000000..de7df44
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/components/user-dropdown.tsx
@@ -0,0 +1,68 @@
+import { useRef } from 'react'
+import { Link, Form } from 'react-router'
+import { getUserImgSrc } from '#app/utils/misc.tsx'
+import { useUser } from '#app/utils/user.ts'
+import { Button } from './ui/button'
+import {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuPortal,
+ DropdownMenuContent,
+ DropdownMenuItem,
+} from './ui/dropdown-menu'
+import { Icon } from './ui/icon'
+
+export function UserDropdown() {
+ const user = useUser()
+ const formRef = useRef(null)
+ return (
+
+
+
+ e.preventDefault()}
+ className="flex items-center gap-2"
+ >
+
+
+ {user.name ?? user.username}
+
+
+
+
+
+
+
+
+
+ Profile
+
+
+
+
+
+
+ Notes
+
+
+
+
+
+
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/entry.client.tsx b/examples/federation/epic-stack-remote/app/entry.client.tsx
new file mode 100644
index 0000000..9b7749f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/entry.client.tsx
@@ -0,0 +1,11 @@
+import { startTransition } from 'react'
+import { hydrateRoot } from 'react-dom/client'
+import { HydratedRouter } from 'react-router/dom'
+
+if (ENV.MODE === 'production' && ENV.SENTRY_DSN) {
+ void import('./utils/monitoring.client.tsx').then(({ init }) => init())
+}
+
+startTransition(() => {
+ hydrateRoot(document, )
+})
diff --git a/examples/federation/epic-stack-remote/app/entry.server.tsx b/examples/federation/epic-stack-remote/app/entry.server.tsx
new file mode 100644
index 0000000..59adaf9
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/entry.server.tsx
@@ -0,0 +1,116 @@
+import { PassThrough } from 'node:stream'
+import { createReadableStreamFromReadable } from '@react-router/node'
+
+import * as Sentry from '@sentry/node'
+import chalk from 'chalk'
+import { isbot } from 'isbot'
+import { renderToPipeableStream } from 'react-dom/server'
+import {
+ ServerRouter,
+ type LoaderFunctionArgs,
+ type ActionFunctionArgs,
+ type HandleDocumentRequestFunction,
+} from 'react-router'
+import { getEnv, init } from './utils/env.server.ts'
+import { getInstanceInfo } from './utils/litefs.server.ts'
+import { NonceProvider } from './utils/nonce-provider.ts'
+import { makeTimings } from './utils/timing.server.ts'
+
+export const streamTimeout = 5000
+
+init()
+global.ENV = getEnv()
+
+type DocRequestArgs = Parameters
+
+export default async function handleRequest(...args: DocRequestArgs) {
+ const [
+ request,
+ responseStatusCode,
+ responseHeaders,
+ reactRouterContext,
+ loadContext,
+ ] = args
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ responseHeaders.set('fly-primary-instance', primaryInstance)
+ responseHeaders.set('fly-instance', currentInstance)
+
+ if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
+ responseHeaders.append('Document-Policy', 'js-profiling')
+ }
+
+ const callbackName = isbot(request.headers.get('user-agent'))
+ ? 'onAllReady'
+ : 'onShellReady'
+
+ const nonce = loadContext.cspNonce?.toString() ?? ''
+ return new Promise(async (resolve, reject) => {
+ let didError = false
+ // NOTE: this timing will only include things that are rendered in the shell
+ // and will not include suspended components and deferred loaders
+ const timings = makeTimings('render', 'renderToPipeableStream')
+
+ const { pipe, abort } = renderToPipeableStream(
+
+
+ ,
+ {
+ [callbackName]: () => {
+ const body = new PassThrough()
+ responseHeaders.set('Content-Type', 'text/html')
+ responseHeaders.append('Server-Timing', timings.toString())
+ resolve(
+ new Response(createReadableStreamFromReadable(body), {
+ headers: responseHeaders,
+ status: didError ? 500 : responseStatusCode,
+ }),
+ )
+ pipe(body)
+ },
+ onShellError: (err: unknown) => {
+ reject(err)
+ },
+ onError: () => {
+ didError = true
+ },
+ nonce,
+ },
+ )
+
+ setTimeout(abort, streamTimeout + 5000)
+ })
+}
+
+export async function handleDataRequest(response: Response) {
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ response.headers.set('fly-primary-instance', primaryInstance)
+ response.headers.set('fly-instance', currentInstance)
+
+ return response
+}
+
+export function handleError(
+ error: unknown,
+ { request }: LoaderFunctionArgs | ActionFunctionArgs,
+): void {
+ // Skip capturing if the request is aborted as Remix docs suggest
+ // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
+ if (request.signal.aborted) {
+ return
+ }
+ if (error instanceof Error) {
+ console.error(chalk.red(error.stack))
+ void Sentry.captureException(error)
+ } else {
+ console.error(error)
+ Sentry.captureException(error)
+ }
+}
diff --git a/examples/federation/epic-stack-remote/app/root.tsx b/examples/federation/epic-stack-remote/app/root.tsx
new file mode 100644
index 0000000..da69cdd
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/root.tsx
@@ -0,0 +1,260 @@
+import {
+ data,
+ Link,
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+ useLoaderData,
+ useMatches,
+} from 'react-router'
+import { HoneypotProvider } from 'remix-utils/honeypot/react'
+import { type Route } from './+types/root.ts'
+import appleTouchIconAssetUrl from './assets/favicons/apple-touch-icon.png'
+import faviconAssetUrl from './assets/favicons/favicon.svg'
+import { GeneralErrorBoundary } from './components/error-boundary.tsx'
+import { EpicProgress } from './components/progress-bar.tsx'
+import { SearchBar } from './components/search-bar.tsx'
+import { useToast } from './components/toaster.tsx'
+import { Button } from './components/ui/button.tsx'
+import { href as iconsHref } from './components/ui/icon.tsx'
+import { EpicToaster } from './components/ui/sonner.tsx'
+import { UserDropdown } from './components/user-dropdown.tsx'
+import {
+ ThemeSwitch,
+ useOptionalTheme,
+ useTheme,
+} from './routes/resources+/theme-switch.tsx'
+// import tailwindStyleSheetUrl from './styles/tailwind.css?url'
+import { getUserId, logout } from './utils/auth.server.ts'
+import { ClientHintCheck, getHints } from './utils/client-hints.tsx'
+import { prisma } from './utils/db.server.ts'
+import { getEnv } from './utils/env.server.ts'
+import { pipeHeaders } from './utils/headers.server.ts'
+import { honeypot } from './utils/honeypot.server.ts'
+import { combineHeaders, getDomainUrl } from './utils/misc.tsx'
+import { useNonce } from './utils/nonce-provider.ts'
+import { type Theme, getTheme } from './utils/theme.server.ts'
+import { makeTimings, time } from './utils/timing.server.ts'
+import { getToast } from './utils/toast.server.ts'
+import { useOptionalUser } from './utils/user.ts'
+import "./styles/tailwind.css"
+
+export const links: Route.LinksFunction = () => {
+ return [
+ // Preload svg sprite as a resource to avoid render blocking
+ { rel: 'preload', href: iconsHref, as: 'image' },
+ {
+ rel: 'icon',
+ href: '/favicon.ico',
+ sizes: '48x48',
+ },
+ { rel: 'icon', type: 'image/svg+xml', href: faviconAssetUrl },
+ { rel: 'apple-touch-icon', href: appleTouchIconAssetUrl },
+ {
+ rel: 'manifest',
+ href: '/site.webmanifest',
+ crossOrigin: 'use-credentials',
+ } as const, // necessary to make typescript happy
+ // { rel: 'stylesheet', href: tailwindStyleSheetUrl },
+ ].filter(Boolean)
+}
+
+export const meta: Route.MetaFunction = ({ data }) => {
+ return [
+ { title: data ? 'Epic Notes' : 'Error | Epic Notes' },
+ { name: 'description', content: `Your own captain's log` },
+ ]
+}
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const timings = makeTimings('root loader')
+ const userId = await time(() => getUserId(request), {
+ timings,
+ type: 'getUserId',
+ desc: 'getUserId in root',
+ })
+
+ const user = userId
+ ? await time(
+ () =>
+ prisma.user.findUnique({
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ image: { select: { id: true } },
+ roles: {
+ select: {
+ name: true,
+ permissions: {
+ select: { entity: true, action: true, access: true },
+ },
+ },
+ },
+ },
+ where: { id: userId },
+ }),
+ { timings, type: 'find user', desc: 'find user in root' },
+ )
+ : null
+ if (userId && !user) {
+ console.info('something weird happened')
+ // something weird happened... The user is authenticated but we can't find
+ // them in the database. Maybe they were deleted? Let's log them out.
+ await logout({ request, redirectTo: '/' })
+ }
+ const { toast, headers: toastHeaders } = await getToast(request)
+ const honeyProps = await honeypot.getInputProps()
+
+ return data(
+ {
+ user,
+ requestInfo: {
+ hints: getHints(request),
+ origin: getDomainUrl(request),
+ path: new URL(request.url).pathname,
+ userPrefs: {
+ theme: getTheme(request),
+ },
+ },
+ ENV: getEnv(),
+ toast,
+ honeyProps,
+ },
+ {
+ headers: combineHeaders(
+ { 'Server-Timing': timings.toString() },
+ toastHeaders,
+ ),
+ },
+ )
+}
+
+export const headers: Route.HeadersFunction = pipeHeaders
+
+function Document({
+ children,
+ nonce,
+ theme = 'light',
+ env = {},
+}: {
+ children: React.ReactNode
+ nonce: string
+ theme?: Theme
+ env?: Record
+}) {
+ const allowIndexing = ENV.ALLOW_INDEXING !== 'false'
+ return (
+
+
+
+
+
+
+ {allowIndexing ? null : (
+
+ )}
+
+
+
+ {children}
+
+
+
+
+
+ )
+}
+
+export function Layout({ children }: { children: React.ReactNode }) {
+ // if there was an error running the loader, data could be missing
+ const data = useLoaderData()
+ const nonce = useNonce()
+ const theme = useOptionalTheme()
+ return (
+
+ {children}
+
+ )
+}
+
+function App() {
+ const data = useLoaderData()
+ const user = useOptionalUser()
+ const theme = useTheme()
+ const matches = useMatches()
+ const isOnSearchPage = matches.find((m) => m.id === 'routes/users+/index')
+ const searchBar = isOnSearchPage ? null :
+ useToast(data.toast)
+
+ return (
+ <>
+
+
+
+ >
+ )
+}
+
+function Logo() {
+ return (
+
+
+ epic
+
+
+ notes
+
+
+ )
+}
+
+function AppWithProviders() {
+ const data = useLoaderData()
+ return (
+
+
+
+ )
+}
+
+export default AppWithProviders
+
+// this is a last resort error boundary. There's not much useful information we
+// can offer at this level.
+export const ErrorBoundary = GeneralErrorBoundary
diff --git a/examples/federation/epic-stack-remote/app/routes.ts b/examples/federation/epic-stack-remote/app/routes.ts
new file mode 100644
index 0000000..946ca4a
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes.ts
@@ -0,0 +1,21 @@
+import { type RouteConfig } from '@react-router/dev/routes'
+import { remixRoutesOptionAdapter } from '@react-router/remix-routes-option-adapter'
+import { flatRoutes } from 'remix-flat-routes'
+
+export default remixRoutesOptionAdapter((defineRoutes) => {
+ return flatRoutes('routes', defineRoutes, {
+ ignoredRouteFiles: [
+ '.*',
+ '**/*.css',
+ '**/*.test.{js,jsx,ts,tsx}',
+ '**/__*.*',
+ // This is for server-side utilities you want to colocate
+ // next to your routes without making an additional
+ // directory. If you need a route that includes "server" or
+ // "client" in the filename, use the escape brackets like:
+ // my-route.[server].tsx
+ '**/*.server.*',
+ '**/*.client.*',
+ ],
+ })
+}) satisfies RouteConfig
diff --git a/examples/federation/epic-stack-remote/app/routes/$.tsx b/examples/federation/epic-stack-remote/app/routes/$.tsx
new file mode 100644
index 0000000..52c33e3
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/$.tsx
@@ -0,0 +1,47 @@
+// This is called a "splat route" and as it's in the root `/app/routes/`
+// directory, it's a catchall. If no other routes match, this one will and we
+// can know that the user is hitting a URL that doesn't exist. By throwing a
+// 404 from the loader, we can force the error boundary to render which will
+// ensure the user gets the right status code and we can display a nicer error
+// message for them than the Remix and/or browser default.
+
+import { Link, useLocation } from 'react-router'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+
+export function loader() {
+ throw new Response('Not found', { status: 404 })
+}
+
+export function action() {
+ throw new Response('Not found', { status: 404 })
+}
+
+export default function NotFound() {
+ // due to the loader, this component will never be rendered, but we'll return
+ // the error boundary just in case.
+ return
+}
+
+export function ErrorBoundary() {
+ const location = useLocation()
+ return (
+ (
+
+
+
We can't find this page:
+
+ {location.pathname}
+
+
+
+
Back to home
+
+
+ ),
+ }}
+ />
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_auth+/auth.$provider.callback.test.ts b/examples/federation/epic-stack-remote/app/routes/_auth+/auth.$provider.callback.test.ts
new file mode 100644
index 0000000..443b49b
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_auth+/auth.$provider.callback.test.ts
@@ -0,0 +1,259 @@
+import { invariant } from '@epic-web/invariant'
+import { faker } from '@faker-js/faker'
+import { http } from 'msw'
+import { afterEach, expect, test } from 'vitest'
+import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx'
+import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts'
+import { connectionSessionStorage } from '#app/utils/connections.server.ts'
+import { GITHUB_PROVIDER_NAME } from '#app/utils/connections.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { generateTOTP } from '#app/utils/totp.server.ts'
+import { createUser } from '#tests/db-utils.ts'
+import { insertGitHubUser, deleteGitHubUsers } from '#tests/mocks/github.ts'
+import { server } from '#tests/mocks/index.ts'
+import { consoleError } from '#tests/setup/setup-test-env.ts'
+import { BASE_URL, convertSetCookieToCookie } from '#tests/utils.ts'
+import { loader } from './auth.$provider.callback.ts'
+
+const ROUTE_PATH = '/auth/github/callback'
+const PARAMS = { provider: 'github' }
+
+afterEach(async () => {
+ await deleteGitHubUsers()
+})
+
+test('a new user goes to onboarding', async () => {
+ const request = await setupRequest()
+ const response = await loader({ request, params: PARAMS, context: {} }).catch(
+ (e) => e,
+ )
+ expect(response).toHaveRedirect('/onboarding/github')
+})
+
+test('when auth fails, send the user to login with a toast', async () => {
+ consoleError.mockImplementation(() => {})
+ server.use(
+ http.post('https://github.com/login/oauth/access_token', async () => {
+ return new Response('error', { status: 400 })
+ }),
+ )
+ const request = await setupRequest()
+ const response = await loader({ request, params: PARAMS, context: {} }).catch(
+ (e) => e,
+ )
+ invariant(response instanceof Response, 'response should be a Response')
+ expect(response).toHaveRedirect('/login')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Auth Failed',
+ type: 'error',
+ }),
+ )
+ expect(consoleError).toHaveBeenCalledTimes(1)
+})
+
+test('when a user is logged in, it creates the connection', async () => {
+ const githubUser = await insertGitHubUser()
+ const session = await setupUser()
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Connected',
+ type: 'success',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+ const connection = await prisma.connection.findFirst({
+ select: { id: true },
+ where: {
+ userId: session.userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ expect(
+ connection,
+ 'the connection was not created in the database',
+ ).toBeTruthy()
+})
+
+test(`when a user is logged in and has already connected, it doesn't do anything and just redirects the user back to the connections page`, async () => {
+ const session = await setupUser()
+ const githubUser = await insertGitHubUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ userId: session.userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Already Connected',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+})
+
+test('when a user exists with the same email, create connection and make session', async () => {
+ const githubUser = await insertGitHubUser()
+ const email = githubUser.primaryEmail.toLowerCase()
+ const { userId } = await setupUser({ ...createUser(), email })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+
+ expect(response).toHaveRedirect('/')
+
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ type: 'message',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+
+ const connection = await prisma.connection.findFirst({
+ select: { id: true },
+ where: {
+ userId: userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ expect(
+ connection,
+ 'the connection was not created in the database',
+ ).toBeTruthy()
+
+ await expect(response).toHaveSessionForUser(userId)
+})
+
+test('gives an error if the account is already connected to another user', async () => {
+ const githubUser = await insertGitHubUser()
+ await prisma.user.create({
+ data: {
+ ...createUser(),
+ connections: {
+ create: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ },
+ },
+ },
+ })
+ const session = await setupUser()
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Already Connected',
+ description: expect.stringContaining(
+ 'already connected to another account',
+ ),
+ }),
+ )
+})
+
+test('if a user is not logged in, but the connection exists, make a session', async () => {
+ const githubUser = await insertGitHubUser()
+ const { userId } = await setupUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ userId,
+ },
+ })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/')
+ await expect(response).toHaveSessionForUser(userId)
+})
+
+test('if a user is not logged in, but the connection exists and they have enabled 2FA, send them to verify their 2FA and do not make a session', async () => {
+ const githubUser = await insertGitHubUser()
+ const { userId } = await setupUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ userId,
+ },
+ })
+ const { otp: _otp, ...config } = await generateTOTP()
+ await prisma.verification.create({
+ data: {
+ type: twoFAVerificationType,
+ target: userId,
+ ...config,
+ },
+ })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ const searchParams = new URLSearchParams({
+ type: twoFAVerificationType,
+ target: userId,
+ redirectTo: '/',
+ })
+ expect(response).toHaveRedirect(`/verify?${searchParams}`)
+})
+
+async function setupRequest({
+ sessionId,
+ code = faker.string.uuid(),
+}: { sessionId?: string; code?: string } = {}) {
+ const url = new URL(ROUTE_PATH, BASE_URL)
+ const state = faker.string.uuid()
+ url.searchParams.set('state', state)
+ url.searchParams.set('code', code)
+ const connectionSession = await connectionSessionStorage.getSession()
+ connectionSession.set('oauth2:state', state)
+ const authSession = await authSessionStorage.getSession()
+ if (sessionId) authSession.set(sessionKey, sessionId)
+ const setSessionCookieHeader =
+ await authSessionStorage.commitSession(authSession)
+ const setConnectionSessionCookieHeader =
+ await connectionSessionStorage.commitSession(connectionSession)
+ const request = new Request(url.toString(), {
+ method: 'GET',
+ headers: {
+ cookie: [
+ convertSetCookieToCookie(setConnectionSessionCookieHeader),
+ convertSetCookieToCookie(setSessionCookieHeader),
+ ].join('; '),
+ },
+ })
+ return request
+}
+
+async function setupUser(userData = createUser()) {
+ const session = await prisma.session.create({
+ data: {
+ expirationDate: getSessionExpirationDate(),
+ user: {
+ create: {
+ ...userData,
+ },
+ },
+ },
+ select: {
+ id: true,
+ userId: true,
+ },
+ })
+
+ return session
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_auth+/auth.$provider.callback.ts b/examples/federation/epic-stack-remote/app/routes/_auth+/auth.$provider.callback.ts
new file mode 100644
index 0000000..bbbe564
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_auth+/auth.$provider.callback.ts
@@ -0,0 +1,197 @@
+import { redirect } from 'react-router'
+import {
+ authenticator,
+ getSessionExpirationDate,
+ getUserId,
+} from '#app/utils/auth.server.ts'
+import { ProviderNameSchema, providerLabels } from '#app/utils/connections.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { ensurePrimary } from '#app/utils/litefs.server.ts'
+import { combineHeaders } from '#app/utils/misc.tsx'
+import {
+ normalizeEmail,
+ normalizeUsername,
+} from '#app/utils/providers/provider.ts'
+import {
+ destroyRedirectToHeader,
+ getRedirectCookieValue,
+} from '#app/utils/redirect-cookie.server.ts'
+import {
+ createToastHeaders,
+ redirectWithToast,
+} from '#app/utils/toast.server.ts'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { type Route } from './+types/auth.$provider.callback.ts'
+import { handleNewSession } from './login.server.ts'
+import { onboardingEmailSessionKey } from './onboarding.tsx'
+import { prefilledProfileKey, providerIdKey } from './onboarding_.$provider.tsx'
+
+const destroyRedirectTo = { 'set-cookie': destroyRedirectToHeader }
+
+export async function loader({ request, params }: Route.LoaderArgs) {
+ // this loader performs mutations, so we need to make sure we're on the
+ // primary instance to avoid writing to a read-only replica
+ await ensurePrimary()
+
+ const providerName = ProviderNameSchema.parse(params.provider)
+ const redirectTo = getRedirectCookieValue(request)
+ const label = providerLabels[providerName]
+
+ const authResult = await authenticator
+ .authenticate(providerName, request, { throwOnError: true })
+ .then(
+ (data) =>
+ ({
+ success: true,
+ data,
+ }) as const,
+ (error) =>
+ ({
+ success: false,
+ error,
+ }) as const,
+ )
+
+ if (!authResult.success) {
+ console.error(authResult.error)
+ throw await redirectWithToast(
+ '/login',
+ {
+ title: 'Auth Failed',
+ description: `There was an error authenticating with ${label}.`,
+ type: 'error',
+ },
+ { headers: destroyRedirectTo },
+ )
+ }
+
+ const { data: profile } = authResult
+
+ const existingConnection = await prisma.connection.findUnique({
+ select: { userId: true },
+ where: {
+ providerName_providerId: { providerName, providerId: profile.id },
+ },
+ })
+
+ const userId = await getUserId(request)
+
+ if (existingConnection && userId) {
+ if (existingConnection.userId === userId) {
+ return redirectWithToast(
+ '/settings/profile/connections',
+ {
+ title: 'Already Connected',
+ description: `Your "${profile.username}" ${label} account is already connected.`,
+ },
+ { headers: destroyRedirectTo },
+ )
+ } else {
+ return redirectWithToast(
+ '/settings/profile/connections',
+ {
+ title: 'Already Connected',
+ description: `The "${profile.username}" ${label} account is already connected to another account.`,
+ },
+ { headers: destroyRedirectTo },
+ )
+ }
+ }
+
+ // If we're already logged in, then link the account
+ if (userId) {
+ await prisma.connection.create({
+ data: {
+ providerName,
+ providerId: profile.id,
+ userId,
+ },
+ })
+ return redirectWithToast(
+ '/settings/profile/connections',
+ {
+ title: 'Connected',
+ type: 'success',
+ description: `Your "${profile.username}" ${label} account has been connected.`,
+ },
+ { headers: destroyRedirectTo },
+ )
+ }
+
+ // Connection exists already? Make a new session
+ if (existingConnection) {
+ return makeSession({ request, userId: existingConnection.userId })
+ }
+
+ // if the email matches a user in the db, then link the account and
+ // make a new session
+ const user = await prisma.user.findUnique({
+ select: { id: true },
+ where: { email: profile.email.toLowerCase() },
+ })
+ if (user) {
+ await prisma.connection.create({
+ data: {
+ providerName,
+ providerId: profile.id,
+ userId: user.id,
+ },
+ })
+ return makeSession(
+ { request, userId: user.id },
+ {
+ headers: await createToastHeaders({
+ title: 'Connected',
+ description: `Your "${profile.username}" ${label} account has been connected.`,
+ }),
+ },
+ )
+ }
+
+ // this is a new user, so let's get them onboarded
+ const verifySession = await verifySessionStorage.getSession()
+ verifySession.set(onboardingEmailSessionKey, profile.email)
+ verifySession.set(prefilledProfileKey, {
+ ...profile,
+ email: normalizeEmail(profile.email),
+ username:
+ typeof profile.username === 'string'
+ ? normalizeUsername(profile.username)
+ : undefined,
+ })
+ verifySession.set(providerIdKey, profile.id)
+ const onboardingRedirect = [
+ `/onboarding/${providerName}`,
+ redirectTo ? new URLSearchParams({ redirectTo }) : null,
+ ]
+ .filter(Boolean)
+ .join('?')
+ return redirect(onboardingRedirect, {
+ headers: combineHeaders(
+ { 'set-cookie': await verifySessionStorage.commitSession(verifySession) },
+ destroyRedirectTo,
+ ),
+ })
+}
+
+async function makeSession(
+ {
+ request,
+ userId,
+ redirectTo,
+ }: { request: Request; userId: string; redirectTo?: string | null },
+ responseInit?: ResponseInit,
+) {
+ redirectTo ??= '/'
+ const session = await prisma.session.create({
+ select: { id: true, expirationDate: true, userId: true },
+ data: {
+ expirationDate: getSessionExpirationDate(),
+ userId,
+ },
+ })
+ return handleNewSession(
+ { request, session, redirectTo, remember: true },
+ { headers: combineHeaders(responseInit?.headers, destroyRedirectTo) },
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_auth+/auth_.$provider.ts b/examples/federation/epic-stack-remote/app/routes/_auth+/auth_.$provider.ts
new file mode 100644
index 0000000..b604005
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_auth+/auth_.$provider.ts
@@ -0,0 +1,34 @@
+import { redirect } from 'react-router'
+import { authenticator } from '#app/utils/auth.server.ts'
+import { handleMockAction } from '#app/utils/connections.server.ts'
+import { ProviderNameSchema } from '#app/utils/connections.tsx'
+import { getReferrerRoute } from '#app/utils/misc.tsx'
+import { getRedirectCookieHeader } from '#app/utils/redirect-cookie.server.ts'
+import { type Route } from './+types/auth_.$provider.ts'
+
+export async function loader() {
+ return redirect('/login')
+}
+
+export async function action({ request, params }: Route.ActionArgs) {
+ const providerName = ProviderNameSchema.parse(params.provider)
+
+ try {
+ await handleMockAction(providerName, request)
+ return await authenticator.authenticate(providerName, request)
+ } catch (error: unknown) {
+ if (error instanceof Response) {
+ const formData = await request.formData()
+ const rawRedirectTo = formData.get('redirectTo')
+ const redirectTo =
+ typeof rawRedirectTo === 'string'
+ ? rawRedirectTo
+ : getReferrerRoute(request)
+ const redirectToCookie = getRedirectCookieHeader(redirectTo)
+ if (redirectToCookie) {
+ error.headers.append('set-cookie', redirectToCookie)
+ }
+ }
+ throw error
+ }
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_auth+/forgot-password.tsx b/examples/federation/epic-stack-remote/app/routes/_auth+/forgot-password.tsx
new file mode 100644
index 0000000..0b1c188
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_auth+/forgot-password.tsx
@@ -0,0 +1,189 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import * as E from '@react-email/components'
+import { data, redirect, Link, useFetcher } from 'react-router'
+import { HoneypotInputs } from 'remix-utils/honeypot/react'
+import { z } from 'zod'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { ErrorList, Field } from '#app/components/forms.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { sendEmail } from '#app/utils/email.server.ts'
+import { checkHoneypot } from '#app/utils/honeypot.server.ts'
+import { EmailSchema, UsernameSchema } from '#app/utils/user-validation.ts'
+import { type Route } from './+types/forgot-password.ts'
+import { prepareVerification } from './verify.server.ts'
+
+export const handle: SEOHandle = {
+ getSitemapEntries: () => null,
+}
+
+const ForgotPasswordSchema = z.object({
+ usernameOrEmail: z.union([EmailSchema, UsernameSchema]),
+})
+
+export async function action({ request }: Route.ActionArgs) {
+ const formData = await request.formData()
+ await checkHoneypot(formData)
+ const submission = await parseWithZod(formData, {
+ schema: ForgotPasswordSchema.superRefine(async (data, ctx) => {
+ const user = await prisma.user.findFirst({
+ where: {
+ OR: [
+ { email: data.usernameOrEmail },
+ { username: data.usernameOrEmail },
+ ],
+ },
+ select: { id: true },
+ })
+ if (!user) {
+ ctx.addIssue({
+ path: ['usernameOrEmail'],
+ code: z.ZodIssueCode.custom,
+ message: 'No user exists with this username or email',
+ })
+ return
+ }
+ }),
+ async: true,
+ })
+ if (submission.status !== 'success') {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+ const { usernameOrEmail } = submission.value
+
+ const user = await prisma.user.findFirstOrThrow({
+ where: { OR: [{ email: usernameOrEmail }, { username: usernameOrEmail }] },
+ select: { email: true, username: true },
+ })
+
+ const { verifyUrl, redirectTo, otp } = await prepareVerification({
+ period: 10 * 60,
+ request,
+ type: 'reset-password',
+ target: usernameOrEmail,
+ })
+
+ const response = await sendEmail({
+ to: user.email,
+ subject: `Epic Notes Password Reset`,
+ react: (
+
+ ),
+ })
+
+ if (response.status === 'success') {
+ return redirect(redirectTo.toString())
+ } else {
+ return data(
+ { result: submission.reply({ formErrors: [response.error.message] }) },
+ { status: 500 },
+ )
+ }
+}
+
+function ForgotPasswordEmail({
+ onboardingUrl,
+ otp,
+}: {
+ onboardingUrl: string
+ otp: string
+}) {
+ return (
+
+
+
+ Epic Notes Password Reset
+
+
+
+ Here's your verification code: {otp}
+
+
+
+ Or click the link:
+
+ {onboardingUrl}
+
+
+ )
+}
+
+export const meta: Route.MetaFunction = () => {
+ return [{ title: 'Password Recovery for Epic Notes' }]
+}
+
+export default function ForgotPasswordRoute() {
+ const forgotPassword = useFetcher()
+
+ const [form, fields] = useForm({
+ id: 'forgot-password-form',
+ constraint: getZodConstraint(ForgotPasswordSchema),
+ lastResult: forgotPassword.data?.result,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: ForgotPasswordSchema })
+ },
+ shouldRevalidate: 'onBlur',
+ })
+
+ return (
+
+
+
+
Forgot Password
+
+ No worries, we'll send you reset instructions.
+
+
+
+
+
+
+
+
+
+
+
+
+ Recover password
+
+
+
+
+ Back to Login
+
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_auth+/login.server.ts b/examples/federation/epic-stack-remote/app/routes/_auth+/login.server.ts
new file mode 100644
index 0000000..9d5cf0b
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_auth+/login.server.ts
@@ -0,0 +1,158 @@
+import { invariant } from '@epic-web/invariant'
+import { redirect } from 'react-router'
+import { safeRedirect } from 'remix-utils/safe-redirect'
+import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx'
+import { getUserId, sessionKey } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { combineResponseInits } from '#app/utils/misc.tsx'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { redirectWithToast } from '#app/utils/toast.server.ts'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { getRedirectToUrl, type VerifyFunctionArgs } from './verify.server.ts'
+
+const verifiedTimeKey = 'verified-time'
+const unverifiedSessionIdKey = 'unverified-session-id'
+const rememberKey = 'remember'
+
+export async function handleNewSession(
+ {
+ request,
+ session,
+ redirectTo,
+ remember,
+ }: {
+ request: Request
+ session: { userId: string; id: string; expirationDate: Date }
+ redirectTo?: string
+ remember: boolean
+ },
+ responseInit?: ResponseInit,
+) {
+ const verification = await prisma.verification.findUnique({
+ select: { id: true },
+ where: {
+ target_type: { target: session.userId, type: twoFAVerificationType },
+ },
+ })
+ const userHasTwoFactor = Boolean(verification)
+
+ if (userHasTwoFactor) {
+ const verifySession = await verifySessionStorage.getSession()
+ verifySession.set(unverifiedSessionIdKey, session.id)
+ verifySession.set(rememberKey, remember)
+ const redirectUrl = getRedirectToUrl({
+ request,
+ type: twoFAVerificationType,
+ target: session.userId,
+ redirectTo,
+ })
+ return redirect(
+ `${redirectUrl.pathname}?${redirectUrl.searchParams}`,
+ combineResponseInits(
+ {
+ headers: {
+ 'set-cookie':
+ await verifySessionStorage.commitSession(verifySession),
+ },
+ },
+ responseInit,
+ ),
+ )
+ } else {
+ const authSession = await authSessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ authSession.set(sessionKey, session.id)
+
+ return redirect(
+ safeRedirect(redirectTo),
+ combineResponseInits(
+ {
+ headers: {
+ 'set-cookie': await authSessionStorage.commitSession(authSession, {
+ expires: remember ? session.expirationDate : undefined,
+ }),
+ },
+ },
+ responseInit,
+ ),
+ )
+ }
+}
+
+export async function handleVerification({
+ request,
+ submission,
+}: VerifyFunctionArgs) {
+ invariant(
+ submission.status === 'success',
+ 'Submission should be successful by now',
+ )
+ const authSession = await authSessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const verifySession = await verifySessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+
+ const remember = verifySession.get(rememberKey)
+ const { redirectTo } = submission.value
+ const headers = new Headers()
+ authSession.set(verifiedTimeKey, Date.now())
+
+ const unverifiedSessionId = verifySession.get(unverifiedSessionIdKey)
+ if (unverifiedSessionId) {
+ const session = await prisma.session.findUnique({
+ select: { expirationDate: true },
+ where: { id: unverifiedSessionId },
+ })
+ if (!session) {
+ throw await redirectWithToast('/login', {
+ type: 'error',
+ title: 'Invalid session',
+ description: 'Could not find session to verify. Please try again.',
+ })
+ }
+ authSession.set(sessionKey, unverifiedSessionId)
+
+ headers.append(
+ 'set-cookie',
+ await authSessionStorage.commitSession(authSession, {
+ expires: remember ? session.expirationDate : undefined,
+ }),
+ )
+ } else {
+ headers.append(
+ 'set-cookie',
+ await authSessionStorage.commitSession(authSession),
+ )
+ }
+
+ headers.append(
+ 'set-cookie',
+ await verifySessionStorage.destroySession(verifySession),
+ )
+
+ return redirect(safeRedirect(redirectTo), { headers })
+}
+
+export async function shouldRequestTwoFA(request: Request) {
+ const authSession = await authSessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const verifySession = await verifySessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ if (verifySession.has(unverifiedSessionIdKey)) return true
+ const userId = await getUserId(request)
+ if (!userId) return false
+ // if it's over two hours since they last verified, we should request 2FA again
+ const userHasTwoFA = await prisma.verification.findUnique({
+ select: { id: true },
+ where: { target_type: { target: userId, type: twoFAVerificationType } },
+ })
+ if (!userHasTwoFA) return false
+ const verifiedTime = authSession.get(verifiedTimeKey) ?? new Date(0)
+ const twoHours = 1000 * 60 * 2
+ return Date.now() - verifiedTime > twoHours
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_auth+/login.tsx b/examples/federation/epic-stack-remote/app/routes/_auth+/login.tsx
new file mode 100644
index 0000000..d012449
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_auth+/login.tsx
@@ -0,0 +1,204 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { data, Form, Link, useSearchParams } from 'react-router'
+import { HoneypotInputs } from 'remix-utils/honeypot/react'
+import { z } from 'zod'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { CheckboxField, ErrorList, Field } from '#app/components/forms.tsx'
+import { Spacer } from '#app/components/spacer.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import { login, requireAnonymous } from '#app/utils/auth.server.ts'
+import {
+ ProviderConnectionForm,
+ providerNames,
+} from '#app/utils/connections.tsx'
+import { checkHoneypot } from '#app/utils/honeypot.server.ts'
+import { useIsPending } from '#app/utils/misc.tsx'
+import { PasswordSchema, UsernameSchema } from '#app/utils/user-validation.ts'
+import { type Route } from './+types/login.ts'
+import { handleNewSession } from './login.server.ts'
+
+export const handle: SEOHandle = {
+ getSitemapEntries: () => null,
+}
+
+const LoginFormSchema = z.object({
+ username: UsernameSchema,
+ password: PasswordSchema,
+ redirectTo: z.string().optional(),
+ remember: z.boolean().optional(),
+})
+
+export async function loader({ request }: Route.LoaderArgs) {
+ await requireAnonymous(request)
+ return {}
+}
+
+export async function action({ request }: Route.ActionArgs) {
+ await requireAnonymous(request)
+ const formData = await request.formData()
+ await checkHoneypot(formData)
+ const submission = await parseWithZod(formData, {
+ schema: (intent) =>
+ LoginFormSchema.transform(async (data, ctx) => {
+ if (intent !== null) return { ...data, session: null }
+
+ const session = await login(data)
+ if (!session) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: 'Invalid username or password',
+ })
+ return z.NEVER
+ }
+
+ return { ...data, session }
+ }),
+ async: true,
+ })
+
+ if (submission.status !== 'success' || !submission.value.session) {
+ return data(
+ { result: submission.reply({ hideFields: ['password'] }) },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+
+ const { session, remember, redirectTo } = submission.value
+
+ return handleNewSession({
+ request,
+ session,
+ remember: remember ?? false,
+ redirectTo,
+ })
+}
+
+export default function LoginPage({ actionData }: Route.ComponentProps) {
+ const isPending = useIsPending()
+ const [searchParams] = useSearchParams()
+ const redirectTo = searchParams.get('redirectTo')
+
+ const [form, fields] = useForm({
+ id: 'login-form',
+ constraint: getZodConstraint(LoginFormSchema),
+ defaultValue: { redirectTo },
+ lastResult: actionData?.result,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: LoginFormSchema })
+ },
+ shouldRevalidate: 'onBlur',
+ })
+
+ return (
+
+
+
+
Welcome back!
+
+ Please enter your details.
+
+
+
+
+
+
+
+
+ {providerNames.map((providerName) => (
+
+
+
+ ))}
+
+
+ New here?
+
+ Create an account
+
+
+
+
+
+
+ )
+}
+
+export const meta: Route.MetaFunction = () => {
+ return [{ title: 'Login to Epic Notes' }]
+}
+
+export function ErrorBoundary() {
+ return
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_auth+/logout.tsx b/examples/federation/epic-stack-remote/app/routes/_auth+/logout.tsx
new file mode 100644
index 0000000..e99e670
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_auth+/logout.tsx
@@ -0,0 +1,11 @@
+import { redirect } from 'react-router'
+import { logout } from '#app/utils/auth.server.ts'
+import { type Route } from './+types/logout.ts'
+
+export async function loader() {
+ return redirect('/')
+}
+
+export async function action({ request }: Route.ActionArgs) {
+ return logout({ request })
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_auth+/onboarding.server.ts b/examples/federation/epic-stack-remote/app/routes/_auth+/onboarding.server.ts
new file mode 100644
index 0000000..502ef80
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_auth+/onboarding.server.ts
@@ -0,0 +1,19 @@
+import { invariant } from '@epic-web/invariant'
+import { redirect } from 'react-router'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { onboardingEmailSessionKey } from './onboarding.tsx'
+import { type VerifyFunctionArgs } from './verify.server.ts'
+
+export async function handleVerification({ submission }: VerifyFunctionArgs) {
+ invariant(
+ submission.status === 'success',
+ 'Submission should be successful by now',
+ )
+ const verifySession = await verifySessionStorage.getSession()
+ verifySession.set(onboardingEmailSessionKey, submission.value.target)
+ return redirect('/onboarding', {
+ headers: {
+ 'set-cookie': await verifySessionStorage.commitSession(verifySession),
+ },
+ })
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_auth+/onboarding.tsx b/examples/federation/epic-stack-remote/app/routes/_auth+/onboarding.tsx
new file mode 100644
index 0000000..4c90e76
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_auth+/onboarding.tsx
@@ -0,0 +1,232 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { data, redirect, Form, useSearchParams } from 'react-router'
+import { HoneypotInputs } from 'remix-utils/honeypot/react'
+import { safeRedirect } from 'remix-utils/safe-redirect'
+import { z } from 'zod'
+import { CheckboxField, ErrorList, Field } from '#app/components/forms.tsx'
+import { Spacer } from '#app/components/spacer.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import { requireAnonymous, sessionKey, signup } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { checkHoneypot } from '#app/utils/honeypot.server.ts'
+import { useIsPending } from '#app/utils/misc.tsx'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { redirectWithToast } from '#app/utils/toast.server.ts'
+import {
+ NameSchema,
+ PasswordAndConfirmPasswordSchema,
+ UsernameSchema,
+} from '#app/utils/user-validation.ts'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { type Route } from './+types/onboarding.ts'
+
+export const onboardingEmailSessionKey = 'onboardingEmail'
+
+const SignupFormSchema = z
+ .object({
+ username: UsernameSchema,
+ name: NameSchema,
+ agreeToTermsOfServiceAndPrivacyPolicy: z.boolean({
+ required_error:
+ 'You must agree to the terms of service and privacy policy',
+ }),
+ remember: z.boolean().optional(),
+ redirectTo: z.string().optional(),
+ })
+ .and(PasswordAndConfirmPasswordSchema)
+
+async function requireOnboardingEmail(request: Request) {
+ await requireAnonymous(request)
+ const verifySession = await verifySessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const email = verifySession.get(onboardingEmailSessionKey)
+ if (typeof email !== 'string' || !email) {
+ throw redirect('/signup')
+ }
+ return email
+}
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const email = await requireOnboardingEmail(request)
+ return { email }
+}
+
+export async function action({ request }: Route.ActionArgs) {
+ const email = await requireOnboardingEmail(request)
+ const formData = await request.formData()
+ await checkHoneypot(formData)
+ const submission = await parseWithZod(formData, {
+ schema: (intent) =>
+ SignupFormSchema.superRefine(async (data, ctx) => {
+ const existingUser = await prisma.user.findUnique({
+ where: { username: data.username },
+ select: { id: true },
+ })
+ if (existingUser) {
+ ctx.addIssue({
+ path: ['username'],
+ code: z.ZodIssueCode.custom,
+ message: 'A user already exists with this username',
+ })
+ return
+ }
+ }).transform(async (data) => {
+ if (intent !== null) return { ...data, session: null }
+
+ const session = await signup({ ...data, email })
+ return { ...data, session }
+ }),
+ async: true,
+ })
+
+ if (submission.status !== 'success' || !submission.value.session) {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+
+ const { session, remember, redirectTo } = submission.value
+
+ const authSession = await authSessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ authSession.set(sessionKey, session.id)
+ const verifySession = await verifySessionStorage.getSession()
+ const headers = new Headers()
+ headers.append(
+ 'set-cookie',
+ await authSessionStorage.commitSession(authSession, {
+ expires: remember ? session.expirationDate : undefined,
+ }),
+ )
+ headers.append(
+ 'set-cookie',
+ await verifySessionStorage.destroySession(verifySession),
+ )
+
+ return redirectWithToast(
+ safeRedirect(redirectTo),
+ { title: 'Welcome', description: 'Thanks for signing up!' },
+ { headers },
+ )
+}
+
+export const meta: Route.MetaFunction = () => {
+ return [{ title: 'Setup Epic Notes Account' }]
+}
+
+export default function OnboardingRoute({
+ loaderData,
+ actionData,
+}: Route.ComponentProps) {
+ const isPending = useIsPending()
+ const [searchParams] = useSearchParams()
+ const redirectTo = searchParams.get('redirectTo')
+
+ const [form, fields] = useForm({
+ id: 'onboarding-form',
+ constraint: getZodConstraint(SignupFormSchema),
+ defaultValue: { redirectTo },
+ lastResult: actionData?.result,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: SignupFormSchema })
+ },
+ shouldRevalidate: 'onBlur',
+ })
+
+ return (
+
+
+
+
Welcome aboard {loaderData.email}!
+
+ Please enter your details.
+
+
+
+
+
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_auth+/onboarding_.$provider.server.ts b/examples/federation/epic-stack-remote/app/routes/_auth+/onboarding_.$provider.server.ts
new file mode 100644
index 0000000..502ef80
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_auth+/onboarding_.$provider.server.ts
@@ -0,0 +1,19 @@
+import { invariant } from '@epic-web/invariant'
+import { redirect } from 'react-router'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { onboardingEmailSessionKey } from './onboarding.tsx'
+import { type VerifyFunctionArgs } from './verify.server.ts'
+
+export async function handleVerification({ submission }: VerifyFunctionArgs) {
+ invariant(
+ submission.status === 'success',
+ 'Submission should be successful by now',
+ )
+ const verifySession = await verifySessionStorage.getSession()
+ verifySession.set(onboardingEmailSessionKey, submission.value.target)
+ return redirect('/onboarding', {
+ headers: {
+ 'set-cookie': await verifySessionStorage.commitSession(verifySession),
+ },
+ })
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_auth+/onboarding_.$provider.tsx b/examples/federation/epic-stack-remote/app/routes/_auth+/onboarding_.$provider.tsx
new file mode 100644
index 0000000..7d50398
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_auth+/onboarding_.$provider.tsx
@@ -0,0 +1,280 @@
+import {
+ getFormProps,
+ getInputProps,
+ useForm,
+ type SubmissionResult,
+} from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import {
+ redirect,
+ data,
+ type Params,
+ Form,
+ useSearchParams,
+} from 'react-router'
+import { safeRedirect } from 'remix-utils/safe-redirect'
+import { z } from 'zod'
+import { CheckboxField, ErrorList, Field } from '#app/components/forms.tsx'
+import { Spacer } from '#app/components/spacer.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import {
+ authenticator,
+ sessionKey,
+ signupWithConnection,
+ requireAnonymous,
+} from '#app/utils/auth.server.ts'
+import { connectionSessionStorage } from '#app/utils/connections.server'
+import { ProviderNameSchema } from '#app/utils/connections.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { useIsPending } from '#app/utils/misc.tsx'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { redirectWithToast } from '#app/utils/toast.server.ts'
+import { NameSchema, UsernameSchema } from '#app/utils/user-validation.ts'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { type Route } from './+types/onboarding_.$provider.ts'
+import { onboardingEmailSessionKey } from './onboarding'
+
+export const providerIdKey = 'providerId'
+export const prefilledProfileKey = 'prefilledProfile'
+
+const SignupFormSchema = z.object({
+ imageUrl: z.string().optional(),
+ username: UsernameSchema,
+ name: NameSchema,
+ agreeToTermsOfServiceAndPrivacyPolicy: z.boolean({
+ required_error: 'You must agree to the terms of service and privacy policy',
+ }),
+ remember: z.boolean().optional(),
+ redirectTo: z.string().optional(),
+})
+
+async function requireData({
+ request,
+ params,
+}: {
+ request: Request
+ params: Params
+}) {
+ await requireAnonymous(request)
+ const verifySession = await verifySessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const email = verifySession.get(onboardingEmailSessionKey)
+ const providerId = verifySession.get(providerIdKey)
+ const result = z
+ .object({
+ email: z.string(),
+ providerName: ProviderNameSchema,
+ providerId: z.string(),
+ })
+ .safeParse({ email, providerName: params.provider, providerId })
+ if (result.success) {
+ return result.data
+ } else {
+ console.error(result.error)
+ throw redirect('/signup')
+ }
+}
+
+export async function loader({ request, params }: Route.LoaderArgs) {
+ const { email } = await requireData({ request, params })
+ const connectionSession = await connectionSessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const verifySession = await verifySessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const prefilledProfile = verifySession.get(prefilledProfileKey)
+
+ const formError = connectionSession.get(authenticator.sessionErrorKey)
+ const hasError = typeof formError === 'string'
+
+ return {
+ email,
+ status: 'idle',
+ submission: {
+ status: hasError ? 'error' : undefined,
+ initialValue: prefilledProfile ?? {},
+ error: { '': hasError ? [formError] : [] },
+ } as SubmissionResult,
+ }
+}
+
+export async function action({ request, params }: Route.ActionArgs) {
+ const { email, providerId, providerName } = await requireData({
+ request,
+ params,
+ })
+ const formData = await request.formData()
+ const verifySession = await verifySessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+
+ const submission = await parseWithZod(formData, {
+ schema: SignupFormSchema.superRefine(async (data, ctx) => {
+ const existingUser = await prisma.user.findUnique({
+ where: { username: data.username },
+ select: { id: true },
+ })
+ if (existingUser) {
+ ctx.addIssue({
+ path: ['username'],
+ code: z.ZodIssueCode.custom,
+ message: 'A user already exists with this username',
+ })
+ return
+ }
+ }).transform(async (data) => {
+ const session = await signupWithConnection({
+ ...data,
+ email,
+ providerId,
+ providerName,
+ })
+ return { ...data, session }
+ }),
+ async: true,
+ })
+
+ if (submission.status !== 'success') {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+
+ const { session, remember, redirectTo } = submission.value
+
+ const authSession = await authSessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ authSession.set(sessionKey, session.id)
+ const headers = new Headers()
+ headers.append(
+ 'set-cookie',
+ await authSessionStorage.commitSession(authSession, {
+ expires: remember ? session.expirationDate : undefined,
+ }),
+ )
+ headers.append(
+ 'set-cookie',
+ await verifySessionStorage.destroySession(verifySession),
+ )
+
+ return redirectWithToast(
+ safeRedirect(redirectTo),
+ { title: 'Welcome', description: 'Thanks for signing up!' },
+ { headers },
+ )
+}
+
+export const meta: Route.MetaFunction = () => {
+ return [{ title: 'Setup Epic Notes Account' }]
+}
+
+export default function OnboardingProviderRoute({
+ loaderData,
+ actionData,
+}: Route.ComponentProps) {
+ const isPending = useIsPending()
+ const [searchParams] = useSearchParams()
+ const redirectTo = searchParams.get('redirectTo')
+
+ const [form, fields] = useForm({
+ id: 'onboarding-provider-form',
+ constraint: getZodConstraint(SignupFormSchema),
+ lastResult: actionData?.result ?? loaderData.submission,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: SignupFormSchema })
+ },
+ shouldRevalidate: 'onBlur',
+ })
+
+ return (
+
+
+
+
Welcome aboard {loaderData.email}!
+
+ Please enter your details.
+
+
+
+
+
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_auth+/reset-password.server.ts b/examples/federation/epic-stack-remote/app/routes/_auth+/reset-password.server.ts
new file mode 100644
index 0000000..f5b2d50
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_auth+/reset-password.server.ts
@@ -0,0 +1,34 @@
+import { invariant } from '@epic-web/invariant'
+import { data, redirect } from 'react-router'
+import { prisma } from '#app/utils/db.server.ts'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { resetPasswordUsernameSessionKey } from './reset-password.tsx'
+import { type VerifyFunctionArgs } from './verify.server.ts'
+
+export async function handleVerification({ submission }: VerifyFunctionArgs) {
+ invariant(
+ submission.status === 'success',
+ 'Submission should be successful by now',
+ )
+ const target = submission.value.target
+ const user = await prisma.user.findFirst({
+ where: { OR: [{ email: target }, { username: target }] },
+ select: { email: true, username: true },
+ })
+ // we don't want to say the user is not found if the email is not found
+ // because that would allow an attacker to check if an email is registered
+ if (!user) {
+ return data(
+ { result: submission.reply({ fieldErrors: { code: ['Invalid code'] } }) },
+ { status: 400 },
+ )
+ }
+
+ const verifySession = await verifySessionStorage.getSession()
+ verifySession.set(resetPasswordUsernameSessionKey, user.username)
+ return redirect('/reset-password', {
+ headers: {
+ 'set-cookie': await verifySessionStorage.commitSession(verifySession),
+ },
+ })
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_auth+/reset-password.tsx b/examples/federation/epic-stack-remote/app/routes/_auth+/reset-password.tsx
new file mode 100644
index 0000000..b8581a7
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_auth+/reset-password.tsx
@@ -0,0 +1,137 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { data, redirect, Form } from 'react-router'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { ErrorList, Field } from '#app/components/forms.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import { requireAnonymous, resetUserPassword } from '#app/utils/auth.server.ts'
+import { useIsPending } from '#app/utils/misc.tsx'
+import { PasswordAndConfirmPasswordSchema } from '#app/utils/user-validation.ts'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { type Route } from './+types/reset-password.ts'
+
+export const handle: SEOHandle = {
+ getSitemapEntries: () => null,
+}
+
+export const resetPasswordUsernameSessionKey = 'resetPasswordUsername'
+
+const ResetPasswordSchema = PasswordAndConfirmPasswordSchema
+
+async function requireResetPasswordUsername(request: Request) {
+ await requireAnonymous(request)
+ const verifySession = await verifySessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const resetPasswordUsername = verifySession.get(
+ resetPasswordUsernameSessionKey,
+ )
+ if (typeof resetPasswordUsername !== 'string' || !resetPasswordUsername) {
+ throw redirect('/login')
+ }
+ return resetPasswordUsername
+}
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const resetPasswordUsername = await requireResetPasswordUsername(request)
+ return { resetPasswordUsername }
+}
+
+export async function action({ request }: Route.ActionArgs) {
+ const resetPasswordUsername = await requireResetPasswordUsername(request)
+ const formData = await request.formData()
+ const submission = parseWithZod(formData, {
+ schema: ResetPasswordSchema,
+ })
+ if (submission.status !== 'success') {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+ const { password } = submission.value
+
+ await resetUserPassword({ username: resetPasswordUsername, password })
+ const verifySession = await verifySessionStorage.getSession()
+ return redirect('/login', {
+ headers: {
+ 'set-cookie': await verifySessionStorage.destroySession(verifySession),
+ },
+ })
+}
+
+export const meta: Route.MetaFunction = () => {
+ return [{ title: 'Reset Password | Epic Notes' }]
+}
+
+export default function ResetPasswordPage({
+ loaderData,
+ actionData,
+}: Route.ComponentProps) {
+ const isPending = useIsPending()
+
+ const [form, fields] = useForm({
+ id: 'reset-password',
+ constraint: getZodConstraint(ResetPasswordSchema),
+ lastResult: actionData?.result,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: ResetPasswordSchema })
+ },
+ shouldRevalidate: 'onBlur',
+ })
+
+ return (
+
+
+
Password Reset
+
+ Hi, {loaderData.resetPasswordUsername}. No worries. It happens all the
+ time.
+
+
+
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_auth+/signup.tsx b/examples/federation/epic-stack-remote/app/routes/_auth+/signup.tsx
new file mode 100644
index 0000000..fa11a9b
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_auth+/signup.tsx
@@ -0,0 +1,185 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import * as E from '@react-email/components'
+import { data, redirect, Form, useSearchParams } from 'react-router'
+import { HoneypotInputs } from 'remix-utils/honeypot/react'
+import { z } from 'zod'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { ErrorList, Field } from '#app/components/forms.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import {
+ ProviderConnectionForm,
+ providerNames,
+} from '#app/utils/connections.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { sendEmail } from '#app/utils/email.server.ts'
+import { checkHoneypot } from '#app/utils/honeypot.server.ts'
+import { useIsPending } from '#app/utils/misc.tsx'
+import { EmailSchema } from '#app/utils/user-validation.ts'
+import { type Route } from './+types/signup.ts'
+import { prepareVerification } from './verify.server.ts'
+
+export const handle: SEOHandle = {
+ getSitemapEntries: () => null,
+}
+
+const SignupSchema = z.object({
+ email: EmailSchema,
+})
+
+export async function action({ request }: Route.ActionArgs) {
+ const formData = await request.formData()
+
+ await checkHoneypot(formData)
+
+ const submission = await parseWithZod(formData, {
+ schema: SignupSchema.superRefine(async (data, ctx) => {
+ const existingUser = await prisma.user.findUnique({
+ where: { email: data.email },
+ select: { id: true },
+ })
+ if (existingUser) {
+ ctx.addIssue({
+ path: ['email'],
+ code: z.ZodIssueCode.custom,
+ message: 'A user already exists with this email',
+ })
+ return
+ }
+ }),
+ async: true,
+ })
+ if (submission.status !== 'success') {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+ const { email } = submission.value
+ const { verifyUrl, redirectTo, otp } = await prepareVerification({
+ period: 10 * 60,
+ request,
+ type: 'onboarding',
+ target: email,
+ })
+
+ const response = await sendEmail({
+ to: email,
+ subject: `Welcome to Epic Notes!`,
+ react: ,
+ })
+
+ if (response.status === 'success') {
+ return redirect(redirectTo.toString())
+ } else {
+ return data(
+ {
+ result: submission.reply({ formErrors: [response.error.message] }),
+ },
+ {
+ status: 500,
+ },
+ )
+ }
+}
+
+export function SignupEmail({
+ onboardingUrl,
+ otp,
+}: {
+ onboardingUrl: string
+ otp: string
+}) {
+ return (
+
+
+
+ Welcome to Epic Notes!
+
+
+
+ Here's your verification code: {otp}
+
+
+
+ Or click the link to get started:
+
+ {onboardingUrl}
+
+
+ )
+}
+
+export const meta: Route.MetaFunction = () => {
+ return [{ title: 'Sign Up | Epic Notes' }]
+}
+
+export default function SignupRoute({ actionData }: Route.ComponentProps) {
+ const isPending = useIsPending()
+ const [searchParams] = useSearchParams()
+ const redirectTo = searchParams.get('redirectTo')
+
+ const [form, fields] = useForm({
+ id: 'signup-form',
+ constraint: getZodConstraint(SignupSchema),
+ lastResult: actionData?.result,
+ onValidate({ formData }) {
+ const result = parseWithZod(formData, { schema: SignupSchema })
+ return result
+ },
+ shouldRevalidate: 'onBlur',
+ })
+
+ return (
+
+
+
Let's start your journey!
+
+ Please enter your email.
+
+
+
+
+
+ {providerNames.map((providerName) => (
+
+
+
+ ))}
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_auth+/verify.server.ts b/examples/federation/epic-stack-remote/app/routes/_auth+/verify.server.ts
new file mode 100644
index 0000000..e3b8e0e
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_auth+/verify.server.ts
@@ -0,0 +1,200 @@
+import { type Submission } from '@conform-to/react'
+import { parseWithZod } from '@conform-to/zod'
+import { data } from 'react-router'
+import { z } from 'zod'
+import { handleVerification as handleChangeEmailVerification } from '#app/routes/settings+/profile.change-email.server.tsx'
+import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx'
+import { requireUserId } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { getDomainUrl } from '#app/utils/misc.tsx'
+import { redirectWithToast } from '#app/utils/toast.server.ts'
+import { generateTOTP, verifyTOTP } from '#app/utils/totp.server.ts'
+import { type twoFAVerifyVerificationType } from '../settings+/profile.two-factor.verify.tsx'
+import {
+ handleVerification as handleLoginTwoFactorVerification,
+ shouldRequestTwoFA,
+} from './login.server.ts'
+import { handleVerification as handleOnboardingVerification } from './onboarding.server.ts'
+import { handleVerification as handleResetPasswordVerification } from './reset-password.server.ts'
+import {
+ VerifySchema,
+ codeQueryParam,
+ redirectToQueryParam,
+ targetQueryParam,
+ typeQueryParam,
+ type VerificationTypes,
+} from './verify.tsx'
+
+export type VerifyFunctionArgs = {
+ request: Request
+ submission: Submission<
+ z.input,
+ string[],
+ z.output
+ >
+ body: FormData | URLSearchParams
+}
+
+export function getRedirectToUrl({
+ request,
+ type,
+ target,
+ redirectTo,
+}: {
+ request: Request
+ type: VerificationTypes
+ target: string
+ redirectTo?: string
+}) {
+ const redirectToUrl = new URL(`${getDomainUrl(request)}/verify`)
+ redirectToUrl.searchParams.set(typeQueryParam, type)
+ redirectToUrl.searchParams.set(targetQueryParam, target)
+ if (redirectTo) {
+ redirectToUrl.searchParams.set(redirectToQueryParam, redirectTo)
+ }
+ return redirectToUrl
+}
+
+export async function requireRecentVerification(request: Request) {
+ const userId = await requireUserId(request)
+ const shouldReverify = await shouldRequestTwoFA(request)
+ if (shouldReverify) {
+ const reqUrl = new URL(request.url)
+ const redirectUrl = getRedirectToUrl({
+ request,
+ target: userId,
+ type: twoFAVerificationType,
+ redirectTo: reqUrl.pathname + reqUrl.search,
+ })
+ throw await redirectWithToast(redirectUrl.toString(), {
+ title: 'Please Reverify',
+ description: 'Please reverify your account before proceeding',
+ })
+ }
+}
+
+export async function prepareVerification({
+ period,
+ request,
+ type,
+ target,
+}: {
+ period: number
+ request: Request
+ type: VerificationTypes
+ target: string
+}) {
+ const verifyUrl = getRedirectToUrl({ request, type, target })
+ const redirectTo = new URL(verifyUrl.toString())
+
+ const { otp, ...verificationConfig } = await generateTOTP({
+ algorithm: 'SHA-256',
+ // Leaving off 0, O, and I on purpose to avoid confusing users.
+ charSet: 'ABCDEFGHJKLMNPQRSTUVWXYZ123456789',
+ period,
+ })
+ const verificationData = {
+ type,
+ target,
+ ...verificationConfig,
+ expiresAt: new Date(Date.now() + verificationConfig.period * 1000),
+ }
+ await prisma.verification.upsert({
+ where: { target_type: { target, type } },
+ create: verificationData,
+ update: verificationData,
+ })
+
+ // add the otp to the url we'll email the user.
+ verifyUrl.searchParams.set(codeQueryParam, otp)
+
+ return { otp, redirectTo, verifyUrl }
+}
+
+export async function isCodeValid({
+ code,
+ type,
+ target,
+}: {
+ code: string
+ type: VerificationTypes | typeof twoFAVerifyVerificationType
+ target: string
+}) {
+ const verification = await prisma.verification.findUnique({
+ where: {
+ target_type: { target, type },
+ OR: [{ expiresAt: { gt: new Date() } }, { expiresAt: null }],
+ },
+ select: { algorithm: true, secret: true, period: true, charSet: true },
+ })
+ if (!verification) return false
+ const result = await verifyTOTP({
+ otp: code,
+ ...verification,
+ })
+ if (!result) return false
+
+ return true
+}
+
+export async function validateRequest(
+ request: Request,
+ body: URLSearchParams | FormData,
+) {
+ const submission = await parseWithZod(body, {
+ schema: VerifySchema.superRefine(async (data, ctx) => {
+ const codeIsValid = await isCodeValid({
+ code: data[codeQueryParam],
+ type: data[typeQueryParam],
+ target: data[targetQueryParam],
+ })
+ if (!codeIsValid) {
+ ctx.addIssue({
+ path: ['code'],
+ code: z.ZodIssueCode.custom,
+ message: `Invalid code`,
+ })
+ return
+ }
+ }),
+ async: true,
+ })
+
+ if (submission.status !== 'success') {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+
+ const { value: submissionValue } = submission
+
+ async function deleteVerification() {
+ await prisma.verification.delete({
+ where: {
+ target_type: {
+ type: submissionValue[typeQueryParam],
+ target: submissionValue[targetQueryParam],
+ },
+ },
+ })
+ }
+
+ switch (submissionValue[typeQueryParam]) {
+ case 'reset-password': {
+ await deleteVerification()
+ return handleResetPasswordVerification({ request, body, submission })
+ }
+ case 'onboarding': {
+ await deleteVerification()
+ return handleOnboardingVerification({ request, body, submission })
+ }
+ case 'change-email': {
+ await deleteVerification()
+ return handleChangeEmailVerification({ request, body, submission })
+ }
+ case '2fa': {
+ return handleLoginTwoFactorVerification({ request, body, submission })
+ }
+ }
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_auth+/verify.tsx b/examples/federation/epic-stack-remote/app/routes/_auth+/verify.tsx
new file mode 100644
index 0000000..c726a3b
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_auth+/verify.tsx
@@ -0,0 +1,144 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { Form, useSearchParams } from 'react-router'
+import { HoneypotInputs } from 'remix-utils/honeypot/react'
+import { z } from 'zod'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { ErrorList, OTPField } from '#app/components/forms.tsx'
+import { Spacer } from '#app/components/spacer.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import { checkHoneypot } from '#app/utils/honeypot.server.ts'
+import { useIsPending } from '#app/utils/misc.tsx'
+import { type Route } from './+types/verify.ts'
+import { validateRequest } from './verify.server.ts'
+
+export const handle: SEOHandle = {
+ getSitemapEntries: () => null,
+}
+
+export const codeQueryParam = 'code'
+export const targetQueryParam = 'target'
+export const typeQueryParam = 'type'
+export const redirectToQueryParam = 'redirectTo'
+const types = ['onboarding', 'reset-password', 'change-email', '2fa'] as const
+const VerificationTypeSchema = z.enum(types)
+export type VerificationTypes = z.infer
+
+export const VerifySchema = z.object({
+ [codeQueryParam]: z.string().min(6).max(6),
+ [typeQueryParam]: VerificationTypeSchema,
+ [targetQueryParam]: z.string(),
+ [redirectToQueryParam]: z.string().optional(),
+})
+
+export async function action({ request }: Route.ActionArgs) {
+ const formData = await request.formData()
+ await checkHoneypot(formData)
+ return validateRequest(request, formData)
+}
+
+export default function VerifyRoute({ actionData }: Route.ComponentProps) {
+ const [searchParams] = useSearchParams()
+ const isPending = useIsPending()
+ const parseWithZoddType = VerificationTypeSchema.safeParse(
+ searchParams.get(typeQueryParam),
+ )
+ const type = parseWithZoddType.success ? parseWithZoddType.data : null
+
+ const checkEmail = (
+ <>
+ Check your email
+
+ We've sent you a code to verify your email address.
+
+ >
+ )
+
+ const headings: Record = {
+ onboarding: checkEmail,
+ 'reset-password': checkEmail,
+ 'change-email': checkEmail,
+ '2fa': (
+ <>
+ Check your 2FA app
+
+ Please enter your 2FA code to verify your identity.
+
+ >
+ ),
+ }
+
+ const [form, fields] = useForm({
+ id: 'verify-form',
+ constraint: getZodConstraint(VerifySchema),
+ lastResult: actionData?.result,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: VerifySchema })
+ },
+ defaultValue: {
+ code: searchParams.get(codeQueryParam),
+ type: type,
+ target: searchParams.get(targetQueryParam),
+ redirectTo: searchParams.get(redirectToQueryParam),
+ },
+ })
+
+ return (
+
+
+ {type ? headings[type] : 'Invalid Verification Type'}
+
+
+
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/about.tsx b/examples/federation/epic-stack-remote/app/routes/_marketing+/about.tsx
new file mode 100644
index 0000000..55ef96d
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/about.tsx
@@ -0,0 +1,3 @@
+export default function AboutRoute() {
+ return About page
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/index.tsx b/examples/federation/epic-stack-remote/app/routes/_marketing+/index.tsx
new file mode 100644
index 0000000..c5cc90f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/index.tsx
@@ -0,0 +1,101 @@
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '#app/components/ui/tooltip.tsx'
+import { cn } from '#app/utils/misc.tsx'
+import { type Route } from './+types/index.ts'
+import { logos } from './logos/logos.ts'
+
+export const meta: Route.MetaFunction = () => [{ title: 'Epic Notes' }]
+
+// Tailwind Grid cell classes lookup
+const columnClasses: Record<(typeof logos)[number]['column'], string> = {
+ 1: 'xl:col-start-1',
+ 2: 'xl:col-start-2',
+ 3: 'xl:col-start-3',
+ 4: 'xl:col-start-4',
+ 5: 'xl:col-start-5',
+}
+const rowClasses: Record<(typeof logos)[number]['row'], string> = {
+ 1: 'xl:row-start-1',
+ 2: 'xl:row-start-2',
+ 3: 'xl:row-start-3',
+ 4: 'xl:row-start-4',
+ 5: 'xl:row-start-5',
+ 6: 'xl:row-start-6',
+}
+
+export default function Index() {
+ return (
+
+
+
+
+
+ {logos.map((logo, i) => (
+
+
+
+
+
+
+
+ {logo.alt}
+
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/docker.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/docker.svg
new file mode 100644
index 0000000..b39ecde
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/docker.svg
@@ -0,0 +1,47 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/eslint.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/eslint.svg
new file mode 100644
index 0000000..dd535a8
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/eslint.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/faker.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/faker.svg
new file mode 100644
index 0000000..e290a83
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/faker.svg
@@ -0,0 +1,736 @@
+
+
+
+ Layer 1
+
+ Group
+
+ Group
+
+ Shape 272
+
+
+
+
+
+
+
+
+
+ Shape 273
+
+
+
+
+
+
+
+
+
+ Shape 274
+
+
+
+
+
+
+
+
+
+ Shape 275
+
+
+
+
+
+
+
+
+
+ Shape 276
+
+
+
+
+
+
+
+
+
+ Shape 277
+
+
+
+
+
+
+
+
+
+ Shape 278
+
+
+
+
+
+
+
+
+
+ Shape 279
+
+
+
+
+
+
+
+
+
+ Shape 280
+
+
+
+
+
+
+
+
+
+ Shape 281
+
+
+
+
+
+
+
+
+
+ Shape 282
+
+
+
+
+
+
+
+
+
+ Shape 283
+
+
+
+
+
+
+
+
+
+ Shape 284
+
+
+
+
+
+
+
+
+
+ Shape 285
+
+
+
+
+
+
+
+
+
+ Shape 286
+
+
+
+
+
+
+
+
+
+ Shape 287
+
+
+
+
+
+
+
+
+
+ Shape 288
+
+
+
+
+
+
+
+
+
+ Shape 289
+
+
+
+
+
+
+
+
+
+ Shape 290
+
+
+
+
+
+
+
+
+
+ Shape 291
+
+
+
+
+
+
+
+
+
+ Shape 292
+
+
+
+
+
+
+
+
+
+ Shape 293
+
+
+
+
+
+
+
+
+
+ Shape 294
+
+
+
+
+
+
+
+
+
+ Shape 295
+
+
+
+
+
+
+
+
+
+ Shape 296
+
+
+
+
+
+
+
+
+
+ Shape 297
+
+
+
+
+
+
+
+
+
+ Shape 298
+
+
+
+
+
+
+
+
+
+ Shape 299
+
+
+
+
+
+
+
+
+
+ Shape 300
+
+
+
+
+
+
+
+
+
+ Shape 301
+
+
+
+
+
+
+
+
+
+ Shape 302
+
+
+
+
+
+
+
+
+
+ Shape 303
+
+
+
+
+
+
+
+
+
+ Shape 304
+
+
+
+
+
+
+
+
+
+ Shape 305
+
+
+
+
+
+
+
+
+
+ Shape 306
+
+
+
+
+
+
+
+
+
+ Shape 307
+
+
+
+
+
+
+
+
+
+ Shape 308
+
+
+
+
+
+
+
+
+
+ Shape 309
+
+
+
+
+
+
+
+
+
+ Shape 310
+
+
+
+
+
+
+
+
+
+ Shape 311
+
+
+
+
+
+
+
+
+
+ Shape 312
+
+
+
+
+
+
+
+
+
+ Shape 313
+
+
+
+
+
+
+
+
+
+ Shape 314
+
+
+
+
+
+
+
+
+
+ Shape 315
+
+
+
+
+
+
+
+
+
+ Shape 316
+
+
+
+
+
+
+
+
+
+ Shape 317
+
+
+
+
+
+
+
+
+
+ Shape 318
+
+
+
+
+
+
+
+
+
+ Shape 319
+
+
+
+
+
+
+
+
+
+ Shape 320
+
+
+
+
+
+
+
+
+
+ Shape 321
+
+
+
+
+
+
+
+
+
+ Shape 322
+
+
+
+
+
+
+
+
+
+ Shape 323
+
+
+
+
+
+
+
+
+
+ Shape 324
+
+
+
+
+
+
+
+
+
+ Shape 325
+
+
+
+
+
+
+
+
+
+ Shape 326
+
+
+
+
+
+
+
+
+
+ Shape 327
+
+
+
+
+
+
+
+
+
+ Shape 328
+
+
+
+
+
+
+
+
+
+ Shape 329
+
+
+
+
+
+
+
+
+
+ Shape 330
+
+
+
+
+
+
+
+
+
+ Shape 331
+
+
+
+
+
+
+
+
+
+ Shape 332
+
+
+
+
+
+
+
+
+
+ Shape 333
+
+
+
+
+
+
+
+
+
+ Shape 334
+
+
+
+
+
+
+
+
+
+ Shape 335
+
+
+
+
+
+
+
+
+
+ Shape 336
+
+
+
+
+
+
+
+
+
+ Shape 337
+
+
+
+
+
+
+
+
+
+ Shape 338
+
+
+
+
+
+
+
+
+
+
+ Shape 339
+
+
+
+
+
+
+
+
+
+
+ Shape 340
+
+
+
+
+
+
+
+
+
+
+ Shape 341
+
+
+
+
+
+
+
+
+
+
+ Shape 342
+
+
+
+
+
+
+
+
+
+ Shape 343
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/fly.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/fly.svg
new file mode 100644
index 0000000..9dbf3c7
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/fly.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/github.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/github.svg
new file mode 100644
index 0000000..37fa923
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/github.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/logos.ts b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/logos.ts
new file mode 100644
index 0000000..7d77e8a
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/logos.ts
@@ -0,0 +1,173 @@
+import docker from './docker.svg'
+import eslint from './eslint.svg'
+import fakerJS from './faker.svg'
+import fly from './fly.svg'
+import github from './github.svg'
+import msw from './msw.svg'
+import playwright from './playwright.svg'
+import prettier from './prettier.svg'
+import prisma from './prisma.svg'
+import radixUI from './radix.svg'
+import reactEmail from './react-email.svg'
+import remix from './remix.svg'
+import resend from './resend.svg'
+import sentry from './sentry.svg'
+import shadcnUI from './shadcn-ui.svg'
+import sqlite from './sqlite.svg'
+import tailwind from './tailwind.svg'
+import testingLibrary from './testing-library.png'
+import typescript from './typescript.svg'
+import vitest from './vitest.svg'
+import zod from './zod.svg'
+
+export { default as stars } from './stars.jpg'
+
+export const logos = [
+ {
+ src: remix,
+ alt: 'Remix',
+ href: 'https://remix.run',
+ column: 1,
+ row: 1,
+ },
+ {
+ src: fly,
+ alt: 'Fly.io',
+ href: 'https://fly.io',
+ column: 1,
+ row: 2,
+ },
+ {
+ src: sqlite,
+ alt: 'SQLite',
+ href: 'https://sqlite.org',
+ column: 1,
+ row: 3,
+ },
+ {
+ src: prisma,
+ alt: 'Prisma',
+ href: 'https://prisma.io',
+ column: 2,
+ row: 2,
+ },
+ {
+ src: zod,
+ alt: 'Zod',
+ href: 'https://zod.dev/',
+ column: 2,
+ row: 3,
+ },
+ {
+ src: github,
+ alt: 'GitHub',
+ href: 'https://github.com',
+ column: 2,
+ row: 4,
+ },
+ {
+ src: resend,
+ alt: 'Resend',
+ href: 'https://resend.com',
+ column: 2,
+ row: 5,
+ },
+ {
+ src: reactEmail,
+ alt: 'React Email',
+ href: 'https://react.email',
+ column: 2,
+ row: 6,
+ },
+ {
+ src: tailwind,
+ alt: 'Tailwind CSS',
+ href: 'https://tailwindcss.com',
+ column: 3,
+ row: 3,
+ },
+ {
+ src: radixUI,
+ alt: 'Radix UI',
+ href: 'https://www.radix-ui.com/',
+ column: 3,
+ row: 4,
+ },
+ {
+ src: shadcnUI,
+ alt: 'shadcn/ui',
+ href: 'https://ui.shadcn.com/',
+ column: 3,
+ row: 5,
+ },
+ {
+ src: playwright,
+ alt: 'Playwright',
+ href: 'https://playwright.dev/',
+ column: 4,
+ row: 1,
+ },
+ {
+ src: msw,
+ alt: 'MSW',
+ href: 'https://mswjs.io',
+ column: 4,
+ row: 2,
+ },
+ {
+ src: fakerJS,
+ alt: 'Faker.js',
+ href: 'https://fakerjs.dev/',
+ column: 4,
+ row: 3,
+ },
+ {
+ src: vitest,
+ alt: 'Vitest',
+ href: 'https://vitest.dev',
+ column: 4,
+ row: 4,
+ },
+ {
+ src: testingLibrary,
+ alt: 'Testing Library',
+ href: 'https://testing-library.com',
+ column: 4,
+ row: 5,
+ },
+ {
+ src: docker,
+ alt: 'Docker',
+ href: 'https://www.docker.com',
+ column: 4,
+ row: 6,
+ },
+ {
+ src: typescript,
+ alt: 'TypeScript',
+ href: 'https://typescriptlang.org',
+ column: 5,
+ row: 2,
+ },
+ {
+ src: prettier,
+ alt: 'Prettier',
+ href: 'https://prettier.io',
+ column: 5,
+ row: 3,
+ },
+ {
+ src: eslint,
+ alt: 'ESLint',
+ href: 'https://eslint.org',
+ column: 5,
+ row: 4,
+ },
+ {
+ src: sentry,
+ alt: 'Sentry',
+ href: 'https://sentry.io',
+ column: 5,
+ row: 5,
+ },
+]
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/msw.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/msw.svg
new file mode 100644
index 0000000..f5de6fb
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/msw.svg
@@ -0,0 +1,13 @@
+
+
+ LOGO
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/playwright.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/playwright.svg
new file mode 100644
index 0000000..7b3ca7d
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/playwright.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/prettier.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/prettier.svg
new file mode 100644
index 0000000..f4d0b66
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/prettier.svg
@@ -0,0 +1,76 @@
+
+
+
+ prettier-icon-dark
+ Created with sketchtool.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/prisma.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/prisma.svg
new file mode 100644
index 0000000..17a3886
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/prisma.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/radix.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/radix.svg
new file mode 100644
index 0000000..a748299
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/radix.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/react-email.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/react-email.svg
new file mode 100644
index 0000000..51a2698
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/react-email.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/remix.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/remix.svg
new file mode 100644
index 0000000..2f4a5ab
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/remix.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/resend.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/resend.svg
new file mode 100644
index 0000000..1227dc5
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/resend.svg
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/sentry.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/sentry.svg
new file mode 100644
index 0000000..3ebb3bb
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/sentry.svg
@@ -0,0 +1,6 @@
+
+
+
\ No newline at end of file
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/shadcn-ui.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/shadcn-ui.svg
new file mode 100644
index 0000000..2780f68
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/shadcn-ui.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/sqlite.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/sqlite.svg
new file mode 100644
index 0000000..424e0ab
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/sqlite.svg
@@ -0,0 +1,67 @@
+
+
+
+SQLite image/svg+xml SQLite
\ No newline at end of file
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/stars.jpg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/stars.jpg
new file mode 100644
index 0000000..c725b8a
Binary files /dev/null and b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/stars.jpg differ
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/tailwind.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/tailwind.svg
new file mode 100644
index 0000000..6a9ab49
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/tailwind.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/testing-library.png b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/testing-library.png
new file mode 100644
index 0000000..6febb2a
Binary files /dev/null and b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/testing-library.png differ
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/typescript.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/typescript.svg
new file mode 100644
index 0000000..339da0b
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/typescript.svg
@@ -0,0 +1,6 @@
+
+
+TypeScript logo
+
+
+
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/vitest.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/vitest.svg
new file mode 100644
index 0000000..fd9daaf
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/vitest.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/zod.svg b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/zod.svg
new file mode 100644
index 0000000..0595f51
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/logos/zod.svg
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/privacy.tsx b/examples/federation/epic-stack-remote/app/routes/_marketing+/privacy.tsx
new file mode 100644
index 0000000..b6d6530
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/privacy.tsx
@@ -0,0 +1,3 @@
+export default function PrivacyRoute() {
+ return Privacy
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/support.tsx b/examples/federation/epic-stack-remote/app/routes/_marketing+/support.tsx
new file mode 100644
index 0000000..ec9a242
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/support.tsx
@@ -0,0 +1,3 @@
+export default function SupportRoute() {
+ return Support
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/tailwind-preset.ts b/examples/federation/epic-stack-remote/app/routes/_marketing+/tailwind-preset.ts
new file mode 100644
index 0000000..f155649
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/tailwind-preset.ts
@@ -0,0 +1,27 @@
+import { type Config } from 'tailwindcss'
+
+export const marketingPreset = {
+ theme: {
+ extend: {
+ keyframes: {
+ 'roll-reveal': {
+ from: { transform: 'rotate(12deg) scale(0)', opacity: '0' },
+ to: { transform: 'rotate(0deg) scale(1)', opacity: '1' },
+ },
+ 'slide-left': {
+ from: { transform: 'translateX(20px)', opacity: '0' },
+ to: { transform: 'translateX(0px)', opacity: '1' },
+ },
+ 'slide-top': {
+ from: { transform: 'translateY(20px)', opacity: '0' },
+ to: { transform: 'translateY(0px)', opacity: '1' },
+ },
+ },
+ animation: {
+ 'roll-reveal': 'roll-reveal 0.4s cubic-bezier(.22,1.28,.54,.99)',
+ 'slide-left': 'slide-left 0.3s ease-out',
+ 'slide-top': 'slide-top 0.3s ease-out',
+ },
+ },
+ },
+} satisfies Omit
diff --git a/examples/federation/epic-stack-remote/app/routes/_marketing+/tos.tsx b/examples/federation/epic-stack-remote/app/routes/_marketing+/tos.tsx
new file mode 100644
index 0000000..998f867
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_marketing+/tos.tsx
@@ -0,0 +1,3 @@
+export default function TermsOfServiceRoute() {
+ return Terms of service
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_seo+/robots[.]txt.ts b/examples/federation/epic-stack-remote/app/routes/_seo+/robots[.]txt.ts
new file mode 100644
index 0000000..314f8ff
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_seo+/robots[.]txt.ts
@@ -0,0 +1,9 @@
+import { generateRobotsTxt } from '@nasa-gcn/remix-seo'
+import { getDomainUrl } from '#app/utils/misc.tsx'
+import { type Route } from './+types/robots[.]txt.ts'
+
+export function loader({ request }: Route.LoaderArgs) {
+ return generateRobotsTxt([
+ { type: 'sitemap', value: `${getDomainUrl(request)}/sitemap.xml` },
+ ])
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/_seo+/sitemap[.]xml.ts b/examples/federation/epic-stack-remote/app/routes/_seo+/sitemap[.]xml.ts
new file mode 100644
index 0000000..04d37c3
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/_seo+/sitemap[.]xml.ts
@@ -0,0 +1,17 @@
+import { generateSitemap } from '@nasa-gcn/remix-seo'
+import { type ServerBuild } from 'react-router'
+import { getDomainUrl } from '#app/utils/misc.tsx'
+import { type Route } from './+types/sitemap[.]xml.ts'
+
+export async function loader({ request, context }: Route.LoaderArgs) {
+ const serverBuild = (await context.serverBuild) as { build: ServerBuild }
+
+ // TODO: This is typeerror is coming up since of the remix-run/server-runtime package. We might need to remove/update that one.
+ // @ts-expect-error
+ return generateSitemap(request, serverBuild.build.routes, {
+ siteUrl: getDomainUrl(request),
+ headers: {
+ 'Cache-Control': `public, max-age=${60 * 5}`,
+ },
+ })
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/admin+/cache.tsx b/examples/federation/epic-stack-remote/app/routes/admin+/cache.tsx
new file mode 100644
index 0000000..8045a9c
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/admin+/cache.tsx
@@ -0,0 +1,244 @@
+import { invariantResponse } from '@epic-web/invariant'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import {
+ redirect,
+ Form,
+ Link,
+ useFetcher,
+ useSearchParams,
+ useSubmit,
+} from 'react-router'
+import { GeneralErrorBoundary } from '#app/components/error-boundary'
+import { Field } from '#app/components/forms.tsx'
+import { Spacer } from '#app/components/spacer.tsx'
+import { Button } from '#app/components/ui/button.tsx'
+import {
+ cache,
+ getAllCacheKeys,
+ lruCache,
+ searchCacheKeys,
+} from '#app/utils/cache.server.ts'
+import {
+ ensureInstance,
+ getAllInstances,
+ getInstanceInfo,
+} from '#app/utils/litefs.server.ts'
+import { useDebounce, useDoubleCheck } from '#app/utils/misc.tsx'
+import { requireUserWithRole } from '#app/utils/permissions.server.ts'
+import { type Route } from './+types/cache.ts'
+
+export const handle: SEOHandle = {
+ getSitemapEntries: () => null,
+}
+
+export async function loader({ request }: Route.LoaderArgs) {
+ await requireUserWithRole(request, 'admin')
+ const searchParams = new URL(request.url).searchParams
+ const query = searchParams.get('query')
+ if (query === '') {
+ searchParams.delete('query')
+ return redirect(`/admin/cache?${searchParams.toString()}`)
+ }
+ const limit = Number(searchParams.get('limit') ?? 100)
+
+ const currentInstanceInfo = await getInstanceInfo()
+ const instance =
+ searchParams.get('instance') ?? currentInstanceInfo.currentInstance
+ const instances = await getAllInstances()
+ await ensureInstance(instance)
+
+ let cacheKeys: { sqlite: Array; lru: Array }
+ if (typeof query === 'string') {
+ cacheKeys = await searchCacheKeys(query, limit)
+ } else {
+ cacheKeys = await getAllCacheKeys(limit)
+ }
+ return { cacheKeys, instance, instances, currentInstanceInfo }
+}
+
+export async function action({ request }: Route.ActionArgs) {
+ await requireUserWithRole(request, 'admin')
+ const formData = await request.formData()
+ const key = formData.get('cacheKey')
+ const { currentInstance } = await getInstanceInfo()
+ const instance = formData.get('instance') ?? currentInstance
+ const type = formData.get('type')
+
+ invariantResponse(typeof key === 'string', 'cacheKey must be a string')
+ invariantResponse(typeof type === 'string', 'type must be a string')
+ invariantResponse(typeof instance === 'string', 'instance must be a string')
+ await ensureInstance(instance)
+
+ switch (type) {
+ case 'sqlite': {
+ await cache.delete(key)
+ break
+ }
+ case 'lru': {
+ lruCache.delete(key)
+ break
+ }
+ default: {
+ throw new Error(`Unknown cache type: ${type}`)
+ }
+ }
+ return { success: true }
+}
+
+export default function CacheAdminRoute({ loaderData }: Route.ComponentProps) {
+ const [searchParams] = useSearchParams()
+ const submit = useSubmit()
+ const query = searchParams.get('query') ?? ''
+ const limit = searchParams.get('limit') ?? '100'
+ const instance = searchParams.get('instance') ?? loaderData.instance
+
+ const handleFormChange = useDebounce(async (form: HTMLFormElement) => {
+ await submit(form)
+ }, 400)
+
+ return (
+
+
Cache Admin
+
+
+
+
+
LRU Cache:
+ {loaderData.cacheKeys.lru.map((key) => (
+
+ ))}
+
+
+
+
SQLite Cache:
+ {loaderData.cacheKeys.sqlite.map((key) => (
+
+ ))}
+
+
+ )
+}
+
+function CacheKeyRow({
+ cacheKey,
+ instance,
+ type,
+}: {
+ cacheKey: string
+ instance?: string
+ type: 'sqlite' | 'lru'
+}) {
+ const fetcher = useFetcher()
+ const dc = useDoubleCheck()
+ const encodedKey = encodeURIComponent(cacheKey)
+ const valuePage = `/admin/cache/${type}/${encodedKey}?instance=${instance}`
+ return (
+
+
+
+
+
+
+ {fetcher.state === 'idle'
+ ? dc.doubleCheck
+ ? 'You sure?'
+ : 'Delete'
+ : 'Deleting...'}
+
+
+
+ {cacheKey}
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return (
+ (
+ You are not allowed to do that: {error?.data.message}
+ ),
+ }}
+ />
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/admin+/cache_.lru.$cacheKey.ts b/examples/federation/epic-stack-remote/app/routes/admin+/cache_.lru.$cacheKey.ts
new file mode 100644
index 0000000..7534705
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/admin+/cache_.lru.$cacheKey.ts
@@ -0,0 +1,31 @@
+import { invariantResponse } from '@epic-web/invariant'
+import { lruCache } from '#app/utils/cache.server.ts'
+import {
+ getAllInstances,
+ getInstanceInfo,
+ ensureInstance,
+} from '#app/utils/litefs.server.ts'
+import { requireUserWithRole } from '#app/utils/permissions.server.ts'
+import { type Route } from './+types/cache_.lru.$cacheKey.ts'
+
+export async function loader({ request, params }: Route.LoaderArgs) {
+ await requireUserWithRole(request, 'admin')
+ const searchParams = new URL(request.url).searchParams
+ const currentInstanceInfo = await getInstanceInfo()
+ const allInstances = await getAllInstances()
+ const instance =
+ searchParams.get('instance') ?? currentInstanceInfo.currentInstance
+ await ensureInstance(instance)
+
+ const { cacheKey } = params
+ invariantResponse(cacheKey, 'cacheKey is required')
+ return {
+ instance: {
+ hostname: instance,
+ region: allInstances[instance],
+ isPrimary: currentInstanceInfo.primaryInstance === instance,
+ },
+ cacheKey,
+ value: lruCache.get(cacheKey),
+ }
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/admin+/cache_.sqlite.$cacheKey.ts b/examples/federation/epic-stack-remote/app/routes/admin+/cache_.sqlite.$cacheKey.ts
new file mode 100644
index 0000000..9aeb3e0
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/admin+/cache_.sqlite.$cacheKey.ts
@@ -0,0 +1,31 @@
+import { invariantResponse } from '@epic-web/invariant'
+import { cache } from '#app/utils/cache.server.ts'
+import {
+ getAllInstances,
+ getInstanceInfo,
+ ensureInstance,
+} from '#app/utils/litefs.server.ts'
+import { requireUserWithRole } from '#app/utils/permissions.server.ts'
+import { type Route } from './+types/cache_.sqlite.$cacheKey.ts'
+
+export async function loader({ request, params }: Route.LoaderArgs) {
+ await requireUserWithRole(request, 'admin')
+ const searchParams = new URL(request.url).searchParams
+ const currentInstanceInfo = await getInstanceInfo()
+ const allInstances = await getAllInstances()
+ const instance =
+ searchParams.get('instance') ?? currentInstanceInfo.currentInstance
+ await ensureInstance(instance)
+
+ const { cacheKey } = params
+ invariantResponse(cacheKey, 'cacheKey is required')
+ return {
+ instance: {
+ hostname: instance,
+ region: allInstances[instance],
+ isPrimary: currentInstanceInfo.primaryInstance === instance,
+ },
+ cacheKey,
+ value: cache.get(cacheKey),
+ }
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/admin+/cache_.sqlite.server.ts b/examples/federation/epic-stack-remote/app/routes/admin+/cache_.sqlite.server.ts
new file mode 100644
index 0000000..04746fc
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/admin+/cache_.sqlite.server.ts
@@ -0,0 +1,59 @@
+import { redirect } from 'react-router'
+import { z } from 'zod'
+import { cache } from '#app/utils/cache.server.ts'
+import {
+ getInstanceInfo,
+ getInternalInstanceDomain,
+} from '#app/utils/litefs.server'
+import { type Route } from './+types/cache_.sqlite.ts'
+
+export async function updatePrimaryCacheValue({
+ key,
+ cacheValue,
+}: {
+ key: string
+ cacheValue: any
+}) {
+ const { currentIsPrimary, primaryInstance } = await getInstanceInfo()
+ if (currentIsPrimary) {
+ throw new Error(
+ `updatePrimaryCacheValue should not be called on the primary instance (${primaryInstance})}`,
+ )
+ }
+ const domain = getInternalInstanceDomain(primaryInstance)
+ const token = process.env.INTERNAL_COMMAND_TOKEN
+ return fetch(`${domain}/admin/cache/sqlite`, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ key, cacheValue }),
+ })
+}
+
+export async function action({ request }: Route.ActionArgs) {
+ const { currentIsPrimary, primaryInstance } = await getInstanceInfo()
+ if (!currentIsPrimary) {
+ throw new Error(
+ `${request.url} should only be called on the primary instance (${primaryInstance})}`,
+ )
+ }
+ const token = process.env.INTERNAL_COMMAND_TOKEN
+ const isAuthorized =
+ request.headers.get('Authorization') === `Bearer ${token}`
+ if (!isAuthorized) {
+ // nah, you can't be here...
+ return redirect('https://www.youtube.com/watch?v=dQw4w9WgXcQ')
+ }
+ const { key, cacheValue } = z
+ .object({ key: z.string(), cacheValue: z.unknown().optional() })
+ .parse(await request.json())
+ if (cacheValue === undefined) {
+ await cache.delete(key)
+ } else {
+ // @ts-expect-error - we don't reliably know the type of cacheValue
+ await cache.set(key, cacheValue)
+ }
+ return { success: true }
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/admin+/cache_.sqlite.tsx b/examples/federation/epic-stack-remote/app/routes/admin+/cache_.sqlite.tsx
new file mode 100644
index 0000000..e469891
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/admin+/cache_.sqlite.tsx
@@ -0,0 +1 @@
+export { action } from './cache_.sqlite.server.ts'
diff --git a/examples/federation/epic-stack-remote/app/routes/me.tsx b/examples/federation/epic-stack-remote/app/routes/me.tsx
new file mode 100644
index 0000000..f7188b5
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/me.tsx
@@ -0,0 +1,19 @@
+import { redirect } from 'react-router'
+import { requireUserId, logout } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { type Route } from './+types/me.ts'
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const userId = await requireUserId(request)
+ const user = await prisma.user.findUnique({ where: { id: userId } })
+ if (!user) {
+ const requestUrl = new URL(request.url)
+ const loginParams = new URLSearchParams([
+ ['redirectTo', `${requestUrl.pathname}${requestUrl.search}`],
+ ])
+ const redirectTo = `/login?${loginParams}`
+ await logout({ request, redirectTo })
+ return redirect(redirectTo)
+ }
+ return redirect(`/users/${user.username}`)
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/resources+/download-user-data.tsx b/examples/federation/epic-stack-remote/app/routes/resources+/download-user-data.tsx
new file mode 100644
index 0000000..c69e9ac
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/resources+/download-user-data.tsx
@@ -0,0 +1,62 @@
+import { requireUserId } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { getDomainUrl } from '#app/utils/misc.tsx'
+import { type Route } from './+types/download-user-data.ts'
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const userId = await requireUserId(request)
+ const user = await prisma.user.findUniqueOrThrow({
+ where: { id: userId },
+ // this is one of the *few* instances where you can use "include" because
+ // the goal is to literally get *everything*. Normally you should be
+ // explicit with "select". We're using select for images because we don't
+ // want to send back the entire blob of the image. We'll send a URL they can
+ // use to download it instead.
+ include: {
+ image: {
+ select: {
+ id: true,
+ createdAt: true,
+ updatedAt: true,
+ contentType: true,
+ },
+ },
+ notes: {
+ include: {
+ images: {
+ select: {
+ id: true,
+ createdAt: true,
+ updatedAt: true,
+ contentType: true,
+ },
+ },
+ },
+ },
+ password: false, // <-- intentionally omit password
+ sessions: true,
+ roles: true,
+ },
+ })
+
+ const domain = getDomainUrl(request)
+
+ return Response.json({
+ user: {
+ ...user,
+ image: user.image
+ ? {
+ ...user.image,
+ url: `${domain}/resources/user-images/${user.image.id}`,
+ }
+ : null,
+ notes: user.notes.map((note) => ({
+ ...note,
+ images: note.images.map((image) => ({
+ ...image,
+ url: `${domain}/resources/note-images/${image.id}`,
+ })),
+ })),
+ },
+ })
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/resources+/healthcheck.tsx b/examples/federation/epic-stack-remote/app/routes/resources+/healthcheck.tsx
new file mode 100644
index 0000000..e38230e
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/resources+/healthcheck.tsx
@@ -0,0 +1,26 @@
+// learn more: https://fly.io/docs/reference/configuration/#services-http_checks
+import { prisma } from '#app/utils/db.server.ts'
+import { type Route } from './+types/healthcheck.ts'
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const host =
+ request.headers.get('X-Forwarded-Host') ?? request.headers.get('host')
+
+ try {
+ // if we can connect to the database and make a simple query
+ // and make a HEAD request to ourselves, then we're good.
+ await Promise.all([
+ prisma.user.count(),
+ fetch(`${new URL(request.url).protocol}${host}`, {
+ method: 'HEAD',
+ headers: { 'X-Healthcheck': 'true' },
+ }).then((r) => {
+ if (!r.ok) return Promise.reject(r)
+ }),
+ ])
+ return new Response('OK')
+ } catch (error: unknown) {
+ console.log('healthcheck β', { error })
+ return new Response('ERROR', { status: 500 })
+ }
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/resources+/note-images.$imageId.tsx b/examples/federation/epic-stack-remote/app/routes/resources+/note-images.$imageId.tsx
new file mode 100644
index 0000000..52c32fb
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/resources+/note-images.$imageId.tsx
@@ -0,0 +1,22 @@
+import { invariantResponse } from '@epic-web/invariant'
+import { prisma } from '#app/utils/db.server.ts'
+import { type Route } from './+types/note-images.$imageId.ts'
+
+export async function loader({ params }: Route.LoaderArgs) {
+ invariantResponse(params.imageId, 'Image ID is required', { status: 400 })
+ const image = await prisma.noteImage.findUnique({
+ where: { id: params.imageId },
+ select: { contentType: true, blob: true },
+ })
+
+ invariantResponse(image, 'Not found', { status: 404 })
+
+ return new Response(image.blob, {
+ headers: {
+ 'Content-Type': image.contentType,
+ 'Content-Length': Buffer.byteLength(image.blob).toString(),
+ 'Content-Disposition': `inline; filename="${params.imageId}"`,
+ 'Cache-Control': 'public, max-age=31536000, immutable',
+ },
+ })
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/resources+/theme-switch.tsx b/examples/federation/epic-stack-remote/app/routes/resources+/theme-switch.tsx
new file mode 100644
index 0000000..bc8d0df
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/resources+/theme-switch.tsx
@@ -0,0 +1,143 @@
+import { useForm, getFormProps } from '@conform-to/react'
+import { parseWithZod } from '@conform-to/zod'
+import { invariantResponse } from '@epic-web/invariant'
+import { data, redirect, useFetcher, useFetchers } from 'react-router'
+import { ServerOnly } from 'remix-utils/server-only'
+import { z } from 'zod'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { useHints, useOptionalHints } from '#app/utils/client-hints.tsx'
+import {
+ useOptionalRequestInfo,
+ useRequestInfo,
+} from '#app/utils/request-info.ts'
+import { type Theme, setTheme } from '#app/utils/theme.server.ts'
+import { type Route } from './+types/theme-switch.ts'
+const ThemeFormSchema = z.object({
+ theme: z.enum(['system', 'light', 'dark']),
+ // this is useful for progressive enhancement
+ redirectTo: z.string().optional(),
+})
+
+export async function action({ request }: Route.ActionArgs) {
+ const formData = await request.formData()
+ const submission = parseWithZod(formData, {
+ schema: ThemeFormSchema,
+ })
+
+ invariantResponse(submission.status === 'success', 'Invalid theme received')
+
+ const { theme, redirectTo } = submission.value
+
+ const responseInit = {
+ headers: { 'set-cookie': setTheme(theme) },
+ }
+ if (redirectTo) {
+ return redirect(redirectTo, responseInit)
+ } else {
+ return data({ result: submission.reply() }, responseInit)
+ }
+}
+
+export function ThemeSwitch({
+ userPreference,
+}: {
+ userPreference?: Theme | null
+}) {
+ const fetcher = useFetcher()
+ const requestInfo = useRequestInfo()
+
+ const [form] = useForm({
+ id: 'theme-switch',
+ lastResult: fetcher.data?.result,
+ })
+
+ const optimisticMode = useOptimisticThemeMode()
+ const mode = optimisticMode ?? userPreference ?? 'system'
+ const nextMode =
+ mode === 'system' ? 'light' : mode === 'light' ? 'dark' : 'system'
+ const modeLabel = {
+ light: (
+
+ Light
+
+ ),
+ dark: (
+
+ Dark
+
+ ),
+ system: (
+
+ System
+
+ ),
+ }
+
+ return (
+
+
+ {() => (
+
+ )}
+
+
+
+
+ {modeLabel[mode]}
+
+
+
+ )
+}
+
+/**
+ * If the user's changing their theme mode preference, this will return the
+ * value it's being changed to.
+ */
+export function useOptimisticThemeMode() {
+ const fetchers = useFetchers()
+ const themeFetcher = fetchers.find(
+ (f) => f.formAction === '/resources/theme-switch',
+ )
+
+ if (themeFetcher && themeFetcher.formData) {
+ const submission = parseWithZod(themeFetcher.formData, {
+ schema: ThemeFormSchema,
+ })
+
+ if (submission.status === 'success') {
+ return submission.value.theme
+ }
+ }
+}
+
+/**
+ * @returns the user's theme preference, or the client hint theme if the user
+ * has not set a preference.
+ */
+export function useTheme() {
+ const hints = useHints()
+ const requestInfo = useRequestInfo()
+ const optimisticMode = useOptimisticThemeMode()
+ if (optimisticMode) {
+ return optimisticMode === 'system' ? hints.theme : optimisticMode
+ }
+ return requestInfo.userPrefs.theme ?? hints.theme
+}
+
+export function useOptionalTheme() {
+ const optionalHints = useOptionalHints()
+ const optionalRequestInfo = useOptionalRequestInfo()
+ const optimisticMode = useOptimisticThemeMode()
+ if (optimisticMode) {
+ return optimisticMode === 'system' ? optionalHints?.theme : optimisticMode
+ }
+ return optionalRequestInfo?.userPrefs.theme ?? optionalHints?.theme
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/resources+/user-images.$imageId.tsx b/examples/federation/epic-stack-remote/app/routes/resources+/user-images.$imageId.tsx
new file mode 100644
index 0000000..0b98cd6
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/resources+/user-images.$imageId.tsx
@@ -0,0 +1,22 @@
+import { invariantResponse } from '@epic-web/invariant'
+import { prisma } from '#app/utils/db.server.ts'
+import { type Route } from './+types/user-images.$imageId.ts'
+
+export async function loader({ params }: Route.LoaderArgs) {
+ invariantResponse(params.imageId, 'Image ID is required', { status: 400 })
+ const image = await prisma.userImage.findUnique({
+ where: { id: params.imageId },
+ select: { contentType: true, blob: true },
+ })
+
+ invariantResponse(image, 'Not found', { status: 404 })
+
+ return new Response(image.blob, {
+ headers: {
+ 'Content-Type': image.contentType,
+ 'Content-Length': Buffer.byteLength(image.blob).toString(),
+ 'Content-Disposition': `inline; filename="${params.imageId}"`,
+ 'Cache-Control': 'public, max-age=31536000, immutable',
+ },
+ })
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/settings+/profile.change-email.server.tsx b/examples/federation/epic-stack-remote/app/routes/settings+/profile.change-email.server.tsx
new file mode 100644
index 0000000..2c31ac4
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/settings+/profile.change-email.server.tsx
@@ -0,0 +1,124 @@
+import { invariant } from '@epic-web/invariant'
+import * as E from '@react-email/components'
+import { data } from 'react-router'
+import {
+ requireRecentVerification,
+ type VerifyFunctionArgs,
+} from '#app/routes/_auth+/verify.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { sendEmail } from '#app/utils/email.server.ts'
+import { redirectWithToast } from '#app/utils/toast.server.ts'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { newEmailAddressSessionKey } from './profile.change-email'
+
+export async function handleVerification({
+ request,
+ submission,
+}: VerifyFunctionArgs) {
+ await requireRecentVerification(request)
+ invariant(
+ submission.status === 'success',
+ 'Submission should be successful by now',
+ )
+
+ const verifySession = await verifySessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const newEmail = verifySession.get(newEmailAddressSessionKey)
+ if (!newEmail) {
+ return data(
+ {
+ result: submission.reply({
+ formErrors: [
+ 'You must submit the code on the same device that requested the email change.',
+ ],
+ }),
+ },
+ { status: 400 },
+ )
+ }
+ const preUpdateUser = await prisma.user.findFirstOrThrow({
+ select: { email: true },
+ where: { id: submission.value.target },
+ })
+ const user = await prisma.user.update({
+ where: { id: submission.value.target },
+ select: { id: true, email: true, username: true },
+ data: { email: newEmail },
+ })
+
+ void sendEmail({
+ to: preUpdateUser.email,
+ subject: 'Epic Stack email changed',
+ react: ,
+ })
+
+ return redirectWithToast(
+ '/settings/profile',
+ {
+ title: 'Email Changed',
+ type: 'success',
+ description: `Your email has been changed to ${user.email}`,
+ },
+ {
+ headers: {
+ 'set-cookie': await verifySessionStorage.destroySession(verifySession),
+ },
+ },
+ )
+}
+
+export function EmailChangeEmail({
+ verifyUrl,
+ otp,
+}: {
+ verifyUrl: string
+ otp: string
+}) {
+ return (
+
+
+
+ Epic Notes Email Change
+
+
+
+ Here's your verification code: {otp}
+
+
+
+ Or click the link:
+
+ {verifyUrl}
+
+
+ )
+}
+
+function EmailChangeNoticeEmail({ userId }: { userId: string }) {
+ return (
+
+
+
+ Your Epic Notes email has been changed
+
+
+
+ We're writing to let you know that your Epic Notes email has been
+ changed.
+
+
+
+
+ If you changed your email address, then you can safely ignore this.
+ But if you did not change your email address, then please contact
+ support immediately.
+
+
+
+ Your Account ID: {userId}
+
+
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/settings+/profile.change-email.tsx b/examples/federation/epic-stack-remote/app/routes/settings+/profile.change-email.tsx
new file mode 100644
index 0000000..47e8676
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/settings+/profile.change-email.tsx
@@ -0,0 +1,146 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { data, redirect, Form } from 'react-router'
+import { z } from 'zod'
+import { ErrorList, Field } from '#app/components/forms.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import {
+ prepareVerification,
+ requireRecentVerification,
+} from '#app/routes/_auth+/verify.server.ts'
+import { requireUserId } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { sendEmail } from '#app/utils/email.server.ts'
+import { useIsPending } from '#app/utils/misc.tsx'
+import { EmailSchema } from '#app/utils/user-validation.ts'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { type Route } from './+types/profile.change-email.ts'
+import { EmailChangeEmail } from './profile.change-email.server.tsx'
+import { type BreadcrumbHandle } from './profile.tsx'
+
+export const handle: BreadcrumbHandle & SEOHandle = {
+ breadcrumb: Change Email ,
+ getSitemapEntries: () => null,
+}
+
+export const newEmailAddressSessionKey = 'new-email-address'
+
+const ChangeEmailSchema = z.object({
+ email: EmailSchema,
+})
+
+export async function loader({ request }: Route.LoaderArgs) {
+ await requireRecentVerification(request)
+ const userId = await requireUserId(request)
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: { email: true },
+ })
+ if (!user) {
+ const params = new URLSearchParams({ redirectTo: request.url })
+ throw redirect(`/login?${params}`)
+ }
+ return { user }
+}
+
+export async function action({ request }: Route.ActionArgs) {
+ const userId = await requireUserId(request)
+ const formData = await request.formData()
+ const submission = await parseWithZod(formData, {
+ schema: ChangeEmailSchema.superRefine(async (data, ctx) => {
+ const existingUser = await prisma.user.findUnique({
+ where: { email: data.email },
+ })
+ if (existingUser) {
+ ctx.addIssue({
+ path: ['email'],
+ code: z.ZodIssueCode.custom,
+ message: 'This email is already in use.',
+ })
+ }
+ }),
+ async: true,
+ })
+
+ if (submission.status !== 'success') {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+ const { otp, redirectTo, verifyUrl } = await prepareVerification({
+ period: 10 * 60,
+ request,
+ target: userId,
+ type: 'change-email',
+ })
+
+ const response = await sendEmail({
+ to: submission.value.email,
+ subject: `Epic Notes Email Change Verification`,
+ react: ,
+ })
+
+ if (response.status === 'success') {
+ const verifySession = await verifySessionStorage.getSession()
+ verifySession.set(newEmailAddressSessionKey, submission.value.email)
+ return redirect(redirectTo.toString(), {
+ headers: {
+ 'set-cookie': await verifySessionStorage.commitSession(verifySession),
+ },
+ })
+ } else {
+ return data(
+ { result: submission.reply({ formErrors: [response.error.message] }) },
+ { status: 500 },
+ )
+ }
+}
+
+export default function ChangeEmailIndex({
+ loaderData,
+ actionData,
+}: Route.ComponentProps) {
+ const [form, fields] = useForm({
+ id: 'change-email-form',
+ constraint: getZodConstraint(ChangeEmailSchema),
+ lastResult: actionData?.result,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: ChangeEmailSchema })
+ },
+ })
+
+ const isPending = useIsPending()
+ return (
+
+
Change Email
+
You will receive an email at the new email address to confirm.
+
+ An email notice will also be sent to your old address{' '}
+ {loaderData.user.email}.
+
+
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/settings+/profile.connections.tsx b/examples/federation/epic-stack-remote/app/routes/settings+/profile.connections.tsx
new file mode 100644
index 0000000..4c38725
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/settings+/profile.connections.tsx
@@ -0,0 +1,212 @@
+import { invariantResponse } from '@epic-web/invariant'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { useState } from 'react'
+import { data, useFetcher } from 'react-router'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '#app/components/ui/tooltip.tsx'
+import { requireUserId } from '#app/utils/auth.server.ts'
+import { resolveConnectionData } from '#app/utils/connections.server.ts'
+import {
+ ProviderConnectionForm,
+ type ProviderName,
+ ProviderNameSchema,
+ providerIcons,
+ providerNames,
+} from '#app/utils/connections.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { pipeHeaders } from '#app/utils/headers.server.js'
+import { makeTimings } from '#app/utils/timing.server.ts'
+import { createToastHeaders } from '#app/utils/toast.server.ts'
+import { type Info, type Route } from './+types/profile.connections.ts'
+import { type BreadcrumbHandle } from './profile.tsx'
+
+export const handle: BreadcrumbHandle & SEOHandle = {
+ breadcrumb: Connections ,
+ getSitemapEntries: () => null,
+}
+
+async function userCanDeleteConnections(userId: string) {
+ const user = await prisma.user.findUnique({
+ select: {
+ password: { select: { userId: true } },
+ _count: { select: { connections: true } },
+ },
+ where: { id: userId },
+ })
+ // user can delete their connections if they have a password
+ if (user?.password) return true
+ // users have to have more than one remaining connection to delete one
+ return Boolean(user?._count.connections && user?._count.connections > 1)
+}
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const userId = await requireUserId(request)
+ const timings = makeTimings('profile connections loader')
+ const rawConnections = await prisma.connection.findMany({
+ select: { id: true, providerName: true, providerId: true, createdAt: true },
+ where: { userId },
+ })
+ const connections: Array<{
+ providerName: ProviderName
+ id: string
+ displayName: string
+ link?: string | null
+ createdAtFormatted: string
+ }> = []
+ for (const connection of rawConnections) {
+ const r = ProviderNameSchema.safeParse(connection.providerName)
+ if (!r.success) continue
+ const providerName = r.data
+ const connectionData = await resolveConnectionData(
+ providerName,
+ connection.providerId,
+ { timings },
+ )
+ connections.push({
+ ...connectionData,
+ providerName,
+ id: connection.id,
+ createdAtFormatted: connection.createdAt.toLocaleString(),
+ })
+ }
+
+ return data(
+ {
+ connections,
+ canDeleteConnections: await userCanDeleteConnections(userId),
+ },
+ { headers: { 'Server-Timing': timings.toString() } },
+ )
+}
+
+export const headers: Route.HeadersFunction = pipeHeaders
+
+export async function action({ request }: Route.ActionArgs) {
+ const userId = await requireUserId(request)
+ const formData = await request.formData()
+ invariantResponse(
+ formData.get('intent') === 'delete-connection',
+ 'Invalid intent',
+ )
+ invariantResponse(
+ await userCanDeleteConnections(userId),
+ 'You cannot delete your last connection unless you have a password.',
+ )
+ const connectionId = formData.get('connectionId')
+ invariantResponse(typeof connectionId === 'string', 'Invalid connectionId')
+ await prisma.connection.delete({
+ where: {
+ id: connectionId,
+ userId: userId,
+ },
+ })
+ const toastHeaders = await createToastHeaders({
+ title: 'Deleted',
+ description: 'Your connection has been deleted.',
+ })
+ return data({ status: 'success' } as const, { headers: toastHeaders })
+}
+
+export default function Connections({ loaderData }: Route.ComponentProps) {
+ return (
+
+ {loaderData.connections.length ? (
+
+
Here are your current connections:
+
+ {loaderData.connections.map((c) => (
+
+
+
+ ))}
+
+
+ ) : (
+
You don't have any connections yet.
+ )}
+
+ {providerNames.map((providerName) => (
+
+ ))}
+
+
+ )
+}
+
+function Connection({
+ connection,
+ canDelete,
+}: {
+ connection: Info['loaderData']['connections'][number]
+ canDelete: boolean
+}) {
+ const deleteFetcher = useFetcher()
+ const [infoOpen, setInfoOpen] = useState(false)
+ const icon = providerIcons[connection.providerName]
+ return (
+
+
+ {icon}
+
+ {connection.link ? (
+
+ {connection.displayName}
+
+ ) : (
+ connection.displayName
+ )}{' '}
+ ({connection.createdAtFormatted})
+
+
+ {canDelete ? (
+
+
+
+
+
+
+
+
+
+ Disconnect this account
+
+
+
+ ) : (
+
+
+ setInfoOpen(true)}>
+
+
+
+ You cannot delete your last connection unless you have a password.
+
+
+
+ )}
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/settings+/profile.index.tsx b/examples/federation/epic-stack-remote/app/routes/settings+/profile.index.tsx
new file mode 100644
index 0000000..49a2150
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/settings+/profile.index.tsx
@@ -0,0 +1,358 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { invariantResponse } from '@epic-web/invariant'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { data, Link, useFetcher } from 'react-router'
+import { z } from 'zod'
+import { ErrorList, Field } from '#app/components/forms.tsx'
+import { Button } from '#app/components/ui/button.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import { requireUserId, sessionKey } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { getUserImgSrc, useDoubleCheck } from '#app/utils/misc.tsx'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { redirectWithToast } from '#app/utils/toast.server.ts'
+import { NameSchema, UsernameSchema } from '#app/utils/user-validation.ts'
+import { type Route, type Info } from './+types/profile.index.ts'
+import { twoFAVerificationType } from './profile.two-factor.tsx'
+
+export const handle: SEOHandle = {
+ getSitemapEntries: () => null,
+}
+
+const ProfileFormSchema = z.object({
+ name: NameSchema.optional(),
+ username: UsernameSchema,
+})
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const userId = await requireUserId(request)
+ const user = await prisma.user.findUniqueOrThrow({
+ where: { id: userId },
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ email: true,
+ image: {
+ select: { id: true },
+ },
+ _count: {
+ select: {
+ sessions: {
+ where: {
+ expirationDate: { gt: new Date() },
+ },
+ },
+ },
+ },
+ },
+ })
+
+ const twoFactorVerification = await prisma.verification.findUnique({
+ select: { id: true },
+ where: { target_type: { type: twoFAVerificationType, target: userId } },
+ })
+
+ const password = await prisma.password.findUnique({
+ select: { userId: true },
+ where: { userId },
+ })
+
+ return {
+ user,
+ hasPassword: Boolean(password),
+ isTwoFactorEnabled: Boolean(twoFactorVerification),
+ }
+}
+
+type ProfileActionArgs = {
+ request: Request
+ userId: string
+ formData: FormData
+}
+const profileUpdateActionIntent = 'update-profile'
+const signOutOfSessionsActionIntent = 'sign-out-of-sessions'
+const deleteDataActionIntent = 'delete-data'
+
+export async function action({ request }: Route.ActionArgs) {
+ const userId = await requireUserId(request)
+ const formData = await request.formData()
+ const intent = formData.get('intent')
+ switch (intent) {
+ case profileUpdateActionIntent: {
+ return profileUpdateAction({ request, userId, formData })
+ }
+ case signOutOfSessionsActionIntent: {
+ return signOutOfSessionsAction({ request, userId, formData })
+ }
+ case deleteDataActionIntent: {
+ return deleteDataAction({ request, userId, formData })
+ }
+ default: {
+ throw new Response(`Invalid intent "${intent}"`, { status: 400 })
+ }
+ }
+}
+
+export default function EditUserProfile({ loaderData }: Route.ComponentProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Change email from {loaderData.user.email}
+
+
+
+
+
+ {loaderData.isTwoFactorEnabled ? (
+ 2FA is enabled
+ ) : (
+ Enable 2FA
+ )}
+
+
+
+
+
+ {loaderData.hasPassword ? 'Change Password' : 'Create a Password'}
+
+
+
+
+
+ Manage connections
+
+
+
+
+ Download your data
+
+
+
+
+
+
+ )
+}
+
+async function profileUpdateAction({ userId, formData }: ProfileActionArgs) {
+ const submission = await parseWithZod(formData, {
+ async: true,
+ schema: ProfileFormSchema.superRefine(async ({ username }, ctx) => {
+ const existingUsername = await prisma.user.findUnique({
+ where: { username },
+ select: { id: true },
+ })
+ if (existingUsername && existingUsername.id !== userId) {
+ ctx.addIssue({
+ path: ['username'],
+ code: z.ZodIssueCode.custom,
+ message: 'A user already exists with this username',
+ })
+ }
+ }),
+ })
+ if (submission.status !== 'success') {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+
+ const { username, name } = submission.value
+
+ await prisma.user.update({
+ select: { username: true },
+ where: { id: userId },
+ data: {
+ name: name,
+ username: username,
+ },
+ })
+
+ return {
+ result: submission.reply(),
+ }
+}
+
+function UpdateProfile({ loaderData }: { loaderData: Info['loaderData'] }) {
+ const fetcher = useFetcher()
+
+ const [form, fields] = useForm({
+ id: 'edit-profile',
+ constraint: getZodConstraint(ProfileFormSchema),
+ lastResult: fetcher.data?.result,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: ProfileFormSchema })
+ },
+ defaultValue: {
+ username: loaderData.user.username,
+ name: loaderData.user.name,
+ },
+ })
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ Save changes
+
+
+
+ )
+}
+
+async function signOutOfSessionsAction({ request, userId }: ProfileActionArgs) {
+ const authSession = await authSessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const sessionId = authSession.get(sessionKey)
+ invariantResponse(
+ sessionId,
+ 'You must be authenticated to sign out of other sessions',
+ )
+ await prisma.session.deleteMany({
+ where: {
+ userId,
+ id: { not: sessionId },
+ },
+ })
+ return { status: 'success' } as const
+}
+
+function SignOutOfSessions({
+ loaderData: loaderData,
+}: {
+ loaderData: Info['loaderData']
+}) {
+ const dc = useDoubleCheck()
+
+ const fetcher = useFetcher()
+ const otherSessionsCount = loaderData.user._count.sessions - 1
+ return (
+
+ {otherSessionsCount ? (
+
+
+
+ {dc.doubleCheck
+ ? `Are you sure?`
+ : `Sign out of ${otherSessionsCount} other sessions`}
+
+
+
+ ) : (
+ This is your only session
+ )}
+
+ )
+}
+
+async function deleteDataAction({ userId }: ProfileActionArgs) {
+ await prisma.user.delete({ where: { id: userId } })
+ return redirectWithToast('/', {
+ type: 'success',
+ title: 'Data Deleted',
+ description: 'All of your data has been deleted',
+ })
+}
+
+function DeleteData() {
+ const dc = useDoubleCheck()
+
+ const fetcher = useFetcher()
+ return (
+
+
+
+
+ {dc.doubleCheck ? `Are you sure?` : `Delete all your data`}
+
+
+
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/settings+/profile.password.tsx b/examples/federation/epic-stack-remote/app/routes/settings+/profile.password.tsx
new file mode 100644
index 0000000..6cad1fb
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/settings+/profile.password.tsx
@@ -0,0 +1,173 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { data, redirect, Form, Link } from 'react-router'
+import { z } from 'zod'
+import { ErrorList, Field } from '#app/components/forms.tsx'
+import { Button } from '#app/components/ui/button.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import {
+ getPasswordHash,
+ requireUserId,
+ verifyUserPassword,
+} from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { useIsPending } from '#app/utils/misc.tsx'
+import { redirectWithToast } from '#app/utils/toast.server.ts'
+import { PasswordSchema } from '#app/utils/user-validation.ts'
+import { type Route } from './+types/profile.password.ts'
+import { type BreadcrumbHandle } from './profile.tsx'
+
+export const handle: BreadcrumbHandle & SEOHandle = {
+ breadcrumb: Password ,
+ getSitemapEntries: () => null,
+}
+
+const ChangePasswordForm = z
+ .object({
+ currentPassword: PasswordSchema,
+ newPassword: PasswordSchema,
+ confirmNewPassword: PasswordSchema,
+ })
+ .superRefine(({ confirmNewPassword, newPassword }, ctx) => {
+ if (confirmNewPassword !== newPassword) {
+ ctx.addIssue({
+ path: ['confirmNewPassword'],
+ code: z.ZodIssueCode.custom,
+ message: 'The passwords must match',
+ })
+ }
+ })
+
+async function requirePassword(userId: string) {
+ const password = await prisma.password.findUnique({
+ select: { userId: true },
+ where: { userId },
+ })
+ if (!password) {
+ throw redirect('/settings/profile/password/create')
+ }
+}
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const userId = await requireUserId(request)
+ await requirePassword(userId)
+ return {}
+}
+
+export async function action({ request }: Route.ActionArgs) {
+ const userId = await requireUserId(request)
+ await requirePassword(userId)
+ const formData = await request.formData()
+ const submission = await parseWithZod(formData, {
+ async: true,
+ schema: ChangePasswordForm.superRefine(
+ async ({ currentPassword, newPassword }, ctx) => {
+ if (currentPassword && newPassword) {
+ const user = await verifyUserPassword({ id: userId }, currentPassword)
+ if (!user) {
+ ctx.addIssue({
+ path: ['currentPassword'],
+ code: z.ZodIssueCode.custom,
+ message: 'Incorrect password.',
+ })
+ }
+ }
+ },
+ ),
+ })
+ if (submission.status !== 'success') {
+ return data(
+ {
+ result: submission.reply({
+ hideFields: ['currentPassword', 'newPassword', 'confirmNewPassword'],
+ }),
+ },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+
+ const { newPassword } = submission.value
+
+ await prisma.user.update({
+ select: { username: true },
+ where: { id: userId },
+ data: {
+ password: {
+ update: {
+ hash: await getPasswordHash(newPassword),
+ },
+ },
+ },
+ })
+
+ return redirectWithToast(
+ `/settings/profile`,
+ {
+ type: 'success',
+ title: 'Password Changed',
+ description: 'Your password has been changed.',
+ },
+ { status: 302 },
+ )
+}
+
+export default function ChangePasswordRoute({
+ actionData,
+}: Route.ComponentProps) {
+ const isPending = useIsPending()
+
+ const [form, fields] = useForm({
+ id: 'password-change-form',
+ constraint: getZodConstraint(ChangePasswordForm),
+ lastResult: actionData?.result,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: ChangePasswordForm })
+ },
+ shouldRevalidate: 'onBlur',
+ })
+
+ return (
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/settings+/profile.password_.create.tsx b/examples/federation/epic-stack-remote/app/routes/settings+/profile.password_.create.tsx
new file mode 100644
index 0000000..b882247
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/settings+/profile.password_.create.tsx
@@ -0,0 +1,124 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { data, redirect, Form, Link } from 'react-router'
+import { ErrorList, Field } from '#app/components/forms.tsx'
+import { Button } from '#app/components/ui/button.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import { getPasswordHash, requireUserId } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { useIsPending } from '#app/utils/misc.tsx'
+import { PasswordAndConfirmPasswordSchema } from '#app/utils/user-validation.ts'
+import { type Route } from './+types/profile.password_.create.ts'
+import { type BreadcrumbHandle } from './profile.tsx'
+
+export const handle: BreadcrumbHandle & SEOHandle = {
+ breadcrumb: Password ,
+ getSitemapEntries: () => null,
+}
+
+const CreatePasswordForm = PasswordAndConfirmPasswordSchema
+
+async function requireNoPassword(userId: string) {
+ const password = await prisma.password.findUnique({
+ select: { userId: true },
+ where: { userId },
+ })
+ if (password) {
+ throw redirect('/settings/profile/password')
+ }
+}
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const userId = await requireUserId(request)
+ await requireNoPassword(userId)
+ return {}
+}
+
+export async function action({ request }: Route.ActionArgs) {
+ const userId = await requireUserId(request)
+ await requireNoPassword(userId)
+ const formData = await request.formData()
+ const submission = await parseWithZod(formData, {
+ async: true,
+ schema: CreatePasswordForm,
+ })
+ if (submission.status !== 'success') {
+ return data(
+ {
+ result: submission.reply({
+ hideFields: ['password', 'confirmPassword'],
+ }),
+ },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+
+ const { password } = submission.value
+
+ await prisma.user.update({
+ select: { username: true },
+ where: { id: userId },
+ data: {
+ password: {
+ create: {
+ hash: await getPasswordHash(password),
+ },
+ },
+ },
+ })
+
+ return redirect(`/settings/profile`, { status: 302 })
+}
+
+export default function CreatePasswordRoute({
+ actionData,
+}: Route.ComponentProps) {
+ const isPending = useIsPending()
+
+ const [form, fields] = useForm({
+ id: 'password-create-form',
+ constraint: getZodConstraint(CreatePasswordForm),
+ lastResult: actionData?.result,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: CreatePasswordForm })
+ },
+ shouldRevalidate: 'onBlur',
+ })
+
+ return (
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/settings+/profile.photo.tsx b/examples/federation/epic-stack-remote/app/routes/settings+/profile.photo.tsx
new file mode 100644
index 0000000..9edc034
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/settings+/profile.photo.tsx
@@ -0,0 +1,239 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { invariantResponse } from '@epic-web/invariant'
+import { type FileUpload, parseFormData } from '@mjackson/form-data-parser'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { useState } from 'react'
+import { data, redirect, Form, useNavigation } from 'react-router'
+import { z } from 'zod'
+import { ErrorList } from '#app/components/forms.tsx'
+import { Button } from '#app/components/ui/button.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import { requireUserId } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { uploadHandler } from '#app/utils/file-uploads.server.ts'
+import {
+ getUserImgSrc,
+ useDoubleCheck,
+ useIsPending,
+} from '#app/utils/misc.tsx'
+import { type Route } from './+types/profile.photo.ts'
+import { type BreadcrumbHandle } from './profile.tsx'
+
+export const handle: BreadcrumbHandle & SEOHandle = {
+ breadcrumb: Photo ,
+ getSitemapEntries: () => null,
+}
+
+const MAX_SIZE = 1024 * 1024 * 3 // 3MB
+
+const DeleteImageSchema = z.object({
+ intent: z.literal('delete'),
+})
+
+const NewImageSchema = z.object({
+ intent: z.literal('submit'),
+ photoFile: z
+ .instanceof(File)
+ .refine((file) => file.size > 0, 'Image is required')
+ .refine(
+ (file) => file.size <= MAX_SIZE,
+ 'Image size must be less than 3MB',
+ ),
+})
+
+const PhotoFormSchema = z.discriminatedUnion('intent', [
+ DeleteImageSchema,
+ NewImageSchema,
+])
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const userId = await requireUserId(request)
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ image: { select: { id: true } },
+ },
+ })
+ invariantResponse(user, 'User not found', { status: 404 })
+ return { user }
+}
+
+export async function action({ request }: Route.ActionArgs) {
+ const userId = await requireUserId(request)
+
+ const formData = await parseFormData(
+ request,
+ { maxFileSize: MAX_SIZE },
+ async (file: FileUpload) => uploadHandler(file),
+ )
+ const submission = await parseWithZod(formData, {
+ schema: PhotoFormSchema.transform(async (data) => {
+ if (data.intent === 'delete') return { intent: 'delete' }
+ if (data.photoFile.size <= 0) return z.NEVER
+ return {
+ intent: data.intent,
+ image: {
+ contentType: data.photoFile.type,
+ blob: Buffer.from(await data.photoFile.arrayBuffer()),
+ },
+ }
+ }),
+ async: true,
+ })
+
+ if (submission.status !== 'success') {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+
+ const { image, intent } = submission.value
+
+ if (intent === 'delete') {
+ await prisma.userImage.deleteMany({ where: { userId } })
+ return redirect('/settings/profile')
+ }
+
+ await prisma.$transaction(async ($prisma) => {
+ await $prisma.userImage.deleteMany({ where: { userId } })
+ await $prisma.user.update({
+ where: { id: userId },
+ data: { image: { create: image } },
+ })
+ })
+
+ return redirect('/settings/profile')
+}
+
+export default function PhotoRoute({
+ loaderData,
+ actionData,
+}: Route.ComponentProps) {
+ const doubleCheckDeleteImage = useDoubleCheck()
+
+ const navigation = useNavigation()
+
+ const [form, fields] = useForm({
+ id: 'profile-photo',
+ constraint: getZodConstraint(PhotoFormSchema),
+ lastResult: actionData?.result,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: PhotoFormSchema })
+ },
+ shouldRevalidate: 'onBlur',
+ })
+
+ const isPending = useIsPending()
+ const pendingIntent = isPending ? navigation.formData?.get('intent') : null
+ const lastSubmissionIntent = fields.intent.value
+
+ const [newImageSrc, setNewImageSrc] = useState(null)
+
+ return (
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/settings+/profile.tsx b/examples/federation/epic-stack-remote/app/routes/settings+/profile.tsx
new file mode 100644
index 0000000..8b29472
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/settings+/profile.tsx
@@ -0,0 +1,80 @@
+import { invariantResponse } from '@epic-web/invariant'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { Link, Outlet, useMatches } from 'react-router'
+import { z } from 'zod'
+import { Spacer } from '#app/components/spacer.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { requireUserId } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { cn } from '#app/utils/misc.tsx'
+import { useUser } from '#app/utils/user.ts'
+import { type Route } from './+types/profile.ts'
+
+export const BreadcrumbHandle = z.object({ breadcrumb: z.any() })
+export type BreadcrumbHandle = z.infer
+
+export const handle: BreadcrumbHandle & SEOHandle = {
+ breadcrumb: Edit Profile ,
+ getSitemapEntries: () => null,
+}
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const userId = await requireUserId(request)
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: { username: true },
+ })
+ invariantResponse(user, 'User not found', { status: 404 })
+ return {}
+}
+
+const BreadcrumbHandleMatch = z.object({
+ handle: BreadcrumbHandle,
+})
+
+export default function EditUserProfile() {
+ const user = useUser()
+ const matches = useMatches()
+ const breadcrumbs = matches
+ .map((m) => {
+ const result = BreadcrumbHandleMatch.safeParse(m)
+ if (!result.success || !result.data.handle.breadcrumb) return null
+ return (
+
+ {result.data.handle.breadcrumb}
+
+ )
+ })
+ .filter(Boolean)
+
+ return (
+
+
+
+
+
+ Profile
+
+
+ {breadcrumbs.map((breadcrumb, i, arr) => (
+
+ βΆοΈ {breadcrumb}
+
+ ))}
+
+
+
+
+
+
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/settings+/profile.two-factor.disable.tsx b/examples/federation/epic-stack-remote/app/routes/settings+/profile.two-factor.disable.tsx
new file mode 100644
index 0000000..ed55924
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/settings+/profile.two-factor.disable.tsx
@@ -0,0 +1,62 @@
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { useFetcher } from 'react-router'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import { requireRecentVerification } from '#app/routes/_auth+/verify.server.ts'
+import { requireUserId } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { useDoubleCheck } from '#app/utils/misc.tsx'
+import { redirectWithToast } from '#app/utils/toast.server.ts'
+import { type Route } from './+types/profile.two-factor.disable.ts'
+import { type BreadcrumbHandle } from './profile.tsx'
+import { twoFAVerificationType } from './profile.two-factor.tsx'
+
+export const handle: BreadcrumbHandle & SEOHandle = {
+ breadcrumb: Disable ,
+ getSitemapEntries: () => null,
+}
+
+export async function loader({ request }: Route.LoaderArgs) {
+ await requireRecentVerification(request)
+ return {}
+}
+
+export async function action({ request }: Route.ActionArgs) {
+ await requireRecentVerification(request)
+ const userId = await requireUserId(request)
+ await prisma.verification.delete({
+ where: { target_type: { target: userId, type: twoFAVerificationType } },
+ })
+ return redirectWithToast('/settings/profile/two-factor', {
+ title: '2FA Disabled',
+ description: 'Two factor authentication has been disabled.',
+ })
+}
+
+export default function TwoFactorDisableRoute() {
+ const disable2FAFetcher = useFetcher()
+ const dc = useDoubleCheck()
+
+ return (
+
+
+
+ Disabling two factor authentication is not recommended. However, if
+ you would like to do so, click here:
+
+
+ {dc.doubleCheck ? 'Are you sure?' : 'Disable 2FA'}
+
+
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/settings+/profile.two-factor.index.tsx b/examples/federation/epic-stack-remote/app/routes/settings+/profile.two-factor.index.tsx
new file mode 100644
index 0000000..7e0f8df
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/settings+/profile.two-factor.index.tsx
@@ -0,0 +1,90 @@
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { redirect, Link, useFetcher } from 'react-router'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import { requireUserId } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { generateTOTP } from '#app/utils/totp.server.ts'
+import { type Route } from './+types/profile.two-factor.index.ts'
+import { twoFAVerificationType } from './profile.two-factor.tsx'
+import { twoFAVerifyVerificationType } from './profile.two-factor.verify.tsx'
+
+export const handle: SEOHandle = {
+ getSitemapEntries: () => null,
+}
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const userId = await requireUserId(request)
+ const verification = await prisma.verification.findUnique({
+ where: { target_type: { type: twoFAVerificationType, target: userId } },
+ select: { id: true },
+ })
+ return { is2FAEnabled: Boolean(verification) }
+}
+
+export async function action({ request }: Route.ActionArgs) {
+ const userId = await requireUserId(request)
+ const { otp: _otp, ...config } = await generateTOTP()
+ const verificationData = {
+ ...config,
+ type: twoFAVerifyVerificationType,
+ target: userId,
+ }
+ await prisma.verification.upsert({
+ where: {
+ target_type: { target: userId, type: twoFAVerifyVerificationType },
+ },
+ create: verificationData,
+ update: verificationData,
+ })
+ return redirect('/settings/profile/two-factor/verify')
+}
+
+export default function TwoFactorRoute({ loaderData }: Route.ComponentProps) {
+ const enable2FAFetcher = useFetcher()
+
+ return (
+
+ {loaderData.is2FAEnabled ? (
+ <>
+
+
+ You have enabled two-factor authentication.
+
+
+
+
Disable 2FA
+
+ >
+ ) : (
+ <>
+
+
+ You have not enabled two-factor authentication yet.
+
+
+
+ Two factor authentication adds an extra layer of security to your
+ account. You will need to enter a code from an authenticator app
+ like{' '}
+
+ 1Password
+ {' '}
+ to log in.
+
+
+
+ Enable 2FA
+
+
+ >
+ )}
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/settings+/profile.two-factor.tsx b/examples/federation/epic-stack-remote/app/routes/settings+/profile.two-factor.tsx
new file mode 100644
index 0000000..0a5f2fa
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/settings+/profile.two-factor.tsx
@@ -0,0 +1,16 @@
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { Outlet } from 'react-router'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { type VerificationTypes } from '#app/routes/_auth+/verify.tsx'
+import { type BreadcrumbHandle } from './profile.tsx'
+
+export const handle: BreadcrumbHandle & SEOHandle = {
+ breadcrumb: 2FA ,
+ getSitemapEntries: () => null,
+}
+
+export const twoFAVerificationType = '2fa' satisfies VerificationTypes
+
+export default function TwoFactorRoute() {
+ return
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/settings+/profile.two-factor.verify.tsx b/examples/federation/epic-stack-remote/app/routes/settings+/profile.two-factor.verify.tsx
new file mode 100644
index 0000000..4949260
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/settings+/profile.two-factor.verify.tsx
@@ -0,0 +1,229 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import * as QRCode from 'qrcode'
+import { data, redirect, Form, useNavigation } from 'react-router'
+import { z } from 'zod'
+import { ErrorList, OTPField } from '#app/components/forms.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import { isCodeValid } from '#app/routes/_auth+/verify.server.ts'
+import { requireUserId } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { getDomainUrl, useIsPending } from '#app/utils/misc.tsx'
+import { redirectWithToast } from '#app/utils/toast.server.ts'
+import { getTOTPAuthUri } from '#app/utils/totp.server.ts'
+import { type Route } from './+types/profile.two-factor.verify.ts'
+import { type BreadcrumbHandle } from './profile.tsx'
+import { twoFAVerificationType } from './profile.two-factor.tsx'
+
+export const handle: BreadcrumbHandle & SEOHandle = {
+ breadcrumb: Verify ,
+ getSitemapEntries: () => null,
+}
+
+const CancelSchema = z.object({ intent: z.literal('cancel') })
+const VerifySchema = z.object({
+ intent: z.literal('verify'),
+ code: z.string().min(6).max(6),
+})
+
+const ActionSchema = z.discriminatedUnion('intent', [
+ CancelSchema,
+ VerifySchema,
+])
+
+export const twoFAVerifyVerificationType = '2fa-verify'
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const userId = await requireUserId(request)
+ const verification = await prisma.verification.findUnique({
+ where: {
+ target_type: { type: twoFAVerifyVerificationType, target: userId },
+ },
+ select: {
+ id: true,
+ algorithm: true,
+ secret: true,
+ period: true,
+ digits: true,
+ },
+ })
+ if (!verification) {
+ return redirect('/settings/profile/two-factor')
+ }
+ const user = await prisma.user.findUniqueOrThrow({
+ where: { id: userId },
+ select: { email: true },
+ })
+ const issuer = new URL(getDomainUrl(request)).host
+ const otpUri = getTOTPAuthUri({
+ ...verification,
+ // OTP clients break with the `-` in the algorithm name.
+ algorithm: verification.algorithm.replaceAll('-', ''),
+ accountName: user.email,
+ issuer,
+ })
+ const qrCode = await QRCode.toDataURL(otpUri)
+ return { otpUri, qrCode }
+}
+
+export async function action({ request }: Route.ActionArgs) {
+ const userId = await requireUserId(request)
+ const formData = await request.formData()
+
+ const submission = await parseWithZod(formData, {
+ schema: () =>
+ ActionSchema.superRefine(async (data, ctx) => {
+ if (data.intent === 'cancel') return null
+ const codeIsValid = await isCodeValid({
+ code: data.code,
+ type: twoFAVerifyVerificationType,
+ target: userId,
+ })
+ if (!codeIsValid) {
+ ctx.addIssue({
+ path: ['code'],
+ code: z.ZodIssueCode.custom,
+ message: `Invalid code`,
+ })
+ return z.NEVER
+ }
+ }),
+ async: true,
+ })
+
+ if (submission.status !== 'success') {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+
+ switch (submission.value.intent) {
+ case 'cancel': {
+ await prisma.verification.deleteMany({
+ where: { type: twoFAVerifyVerificationType, target: userId },
+ })
+ return redirect('/settings/profile/two-factor')
+ }
+ case 'verify': {
+ await prisma.verification.update({
+ where: {
+ target_type: { type: twoFAVerifyVerificationType, target: userId },
+ },
+ data: { type: twoFAVerificationType },
+ })
+ return redirectWithToast('/settings/profile/two-factor', {
+ type: 'success',
+ title: 'Enabled',
+ description: 'Two-factor authentication has been enabled.',
+ })
+ }
+ }
+}
+
+export default function TwoFactorRoute({
+ loaderData,
+ actionData,
+}: Route.ComponentProps) {
+ const navigation = useNavigation()
+
+ const isPending = useIsPending()
+ const pendingIntent = isPending ? navigation.formData?.get('intent') : null
+
+ const [form, fields] = useForm({
+ id: 'verify-form',
+ constraint: getZodConstraint(ActionSchema),
+ lastResult: actionData?.result,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: ActionSchema })
+ },
+ })
+ const lastSubmissionIntent = fields.intent.value
+
+ return (
+
+
+
+
Scan this QR code with your authenticator app.
+
+ If you cannot scan the QR code, you can manually add this account to
+ your authenticator app using this code:
+
+
+
+ {loaderData.otpUri}
+
+
+
+ Once you've added the account, enter the code from your authenticator
+ app below. Once you enable 2FA, you will need to enter a code from
+ your authenticator app every time you log in or perform important
+ actions. Do not lose access to your authenticator app, or you will
+ lose access to your account.
+
+
+
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/users+/$username.test.tsx b/examples/federation/epic-stack-remote/app/routes/users+/$username.test.tsx
new file mode 100644
index 0000000..eee52fb
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/users+/$username.test.tsx
@@ -0,0 +1,95 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { faker } from '@faker-js/faker'
+import { render, screen } from '@testing-library/react'
+import { createRoutesStub } from 'react-router'
+import setCookieParser from 'set-cookie-parser'
+import { test } from 'vitest'
+import { loader as rootLoader } from '#app/root.tsx'
+import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { createUser, getUserImages } from '#tests/db-utils.ts'
+import { default as UsernameRoute, loader } from './$username.tsx'
+
+test('The user profile when not logged in as self', async () => {
+ const userImages = await getUserImages()
+ const userImage =
+ userImages[faker.number.int({ min: 0, max: userImages.length - 1 })]
+ const user = await prisma.user.create({
+ select: { id: true, username: true, name: true },
+ data: { ...createUser(), image: { create: userImage } },
+ })
+ const App = createRoutesStub([
+ {
+ path: '/users/:username',
+ Component: UsernameRoute,
+ loader,
+ },
+ ])
+
+ const routeUrl = `/users/${user.username}`
+ render( )
+
+ await screen.findByRole('heading', { level: 1, name: user.name! })
+ await screen.findByRole('img', { name: user.name! })
+ await screen.findByRole('link', { name: `${user.name}'s notes` })
+})
+
+test('The user profile when logged in as self', async () => {
+ const userImages = await getUserImages()
+ const userImage =
+ userImages[faker.number.int({ min: 0, max: userImages.length - 1 })]
+ const user = await prisma.user.create({
+ select: { id: true, username: true, name: true },
+ data: { ...createUser(), image: { create: userImage } },
+ })
+ const session = await prisma.session.create({
+ select: { id: true },
+ data: {
+ expirationDate: getSessionExpirationDate(),
+ userId: user.id,
+ },
+ })
+
+ const authSession = await authSessionStorage.getSession()
+ authSession.set(sessionKey, session.id)
+ const setCookieHeader = await authSessionStorage.commitSession(authSession)
+ const parsedCookie = setCookieParser.parseString(setCookieHeader)
+ const cookieHeader = new URLSearchParams({
+ [parsedCookie.name]: parsedCookie.value,
+ }).toString()
+
+ const App = createRoutesStub([
+ {
+ id: 'root',
+ path: '/',
+ loader: async (args) => {
+ // add the cookie header to the request
+ args.request.headers.set('cookie', cookieHeader)
+ return rootLoader({ ...args, context: args.context })
+ },
+ children: [
+ {
+ path: 'users/:username',
+ Component: UsernameRoute,
+ loader: async (args) => {
+ // add the cookie header to the request
+ args.request.headers.set('cookie', cookieHeader)
+ return loader(args)
+ },
+ },
+ ],
+ },
+ ])
+
+ const routeUrl = `/users/${user.username}`
+ await render( )
+
+ await screen.findByRole('heading', { level: 1, name: user.name! })
+ await screen.findByRole('img', { name: user.name! })
+ await screen.findByRole('button', { name: /logout/i })
+ await screen.findByRole('link', { name: /my notes/i })
+ await screen.findByRole('link', { name: /edit profile/i })
+})
diff --git a/examples/federation/epic-stack-remote/app/routes/users+/$username.tsx b/examples/federation/epic-stack-remote/app/routes/users+/$username.tsx
new file mode 100644
index 0000000..ff050e1
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/users+/$username.tsx
@@ -0,0 +1,127 @@
+import { invariantResponse } from '@epic-web/invariant'
+import {
+ type LoaderFunctionArgs,
+ Form,
+ Link,
+ useLoaderData,
+} from 'react-router'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { Spacer } from '#app/components/spacer.tsx'
+import { Button } from '#app/components/ui/button.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { getUserImgSrc } from '#app/utils/misc.tsx'
+import { useOptionalUser } from '#app/utils/user.ts'
+import { type Route } from './+types/$username.ts'
+
+export async function loader({ params }: LoaderFunctionArgs) {
+ const user = await prisma.user.findFirst({
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ createdAt: true,
+ image: { select: { id: true } },
+ },
+ where: {
+ username: params.username,
+ },
+ })
+
+ invariantResponse(user, 'User not found', { status: 404 })
+
+ return { user, userJoinedDisplay: user.createdAt.toLocaleDateString() }
+}
+
+export default function ProfileRoute() {
+ const data = useLoaderData()
+ const user = data.user
+ const userDisplayName = user.name ?? user.username
+ const loggedInUser = useOptionalUser()
+ const isLoggedInUser = user.id === loggedInUser?.id
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{userDisplayName}
+
+
+ Joined {data.userJoinedDisplay}
+
+ {isLoggedInUser ? (
+
+ ) : null}
+
+ {isLoggedInUser ? (
+ <>
+
+
+ My notes
+
+
+
+
+ Edit profile
+
+
+ >
+ ) : (
+
+
+ {userDisplayName}'s notes
+
+
+ )}
+
+
+
+
+ )
+}
+
+export const meta: Route.MetaFunction = ({ data, params }) => {
+ const displayName = data?.user.name ?? params.username
+ return [
+ { title: `${displayName} | Epic Notes` },
+ {
+ name: 'description',
+ content: `Profile of ${displayName} on Epic Notes`,
+ },
+ ]
+}
+
+export function ErrorBoundary() {
+ return (
+ (
+ No user with the username "{params.username}" exists
+ ),
+ }}
+ />
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/users+/$username_+/__note-editor.server.tsx b/examples/federation/epic-stack-remote/app/routes/users+/$username_+/__note-editor.server.tsx
new file mode 100644
index 0000000..20a1825
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/users+/$username_+/__note-editor.server.tsx
@@ -0,0 +1,128 @@
+import { parseWithZod } from '@conform-to/zod'
+import { type FileUpload, parseFormData } from '@mjackson/form-data-parser'
+import { createId as cuid } from '@paralleldrive/cuid2'
+import { data, redirect, type ActionFunctionArgs } from 'react-router'
+import { z } from 'zod'
+import { requireUserId } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { uploadHandler } from '#app/utils/file-uploads.server.ts'
+import {
+ MAX_UPLOAD_SIZE,
+ NoteEditorSchema,
+ type ImageFieldset,
+} from './__note-editor'
+
+function imageHasFile(
+ image: ImageFieldset,
+): image is ImageFieldset & { file: NonNullable } {
+ return Boolean(image.file?.size && image.file?.size > 0)
+}
+
+function imageHasId(
+ image: ImageFieldset,
+): image is ImageFieldset & { id: NonNullable } {
+ return image.id != null
+}
+
+export async function action({ request }: ActionFunctionArgs) {
+ const userId = await requireUserId(request)
+
+ const formData = await parseFormData(
+ request,
+ { maxFileSize: MAX_UPLOAD_SIZE },
+ async (file: FileUpload) => uploadHandler(file),
+ )
+
+ const submission = await parseWithZod(formData, {
+ schema: NoteEditorSchema.superRefine(async (data, ctx) => {
+ if (!data.id) return
+
+ const note = await prisma.note.findUnique({
+ select: { id: true },
+ where: { id: data.id, ownerId: userId },
+ })
+ if (!note) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: 'Note not found',
+ })
+ }
+ }).transform(async ({ images = [], ...data }) => {
+ return {
+ ...data,
+ imageUpdates: await Promise.all(
+ images.filter(imageHasId).map(async (i) => {
+ if (imageHasFile(i)) {
+ return {
+ id: i.id,
+ altText: i.altText,
+ contentType: i.file.type,
+ blob: Buffer.from(await i.file.arrayBuffer()),
+ }
+ } else {
+ return {
+ id: i.id,
+ altText: i.altText,
+ }
+ }
+ }),
+ ),
+ newImages: await Promise.all(
+ images
+ .filter(imageHasFile)
+ .filter((i) => !i.id)
+ .map(async (image) => {
+ return {
+ altText: image.altText,
+ contentType: image.file.type,
+ blob: Buffer.from(await image.file.arrayBuffer()),
+ }
+ }),
+ ),
+ }
+ }),
+ async: true,
+ })
+
+ if (submission.status !== 'success') {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+
+ const {
+ id: noteId,
+ title,
+ content,
+ imageUpdates = [],
+ newImages = [],
+ } = submission.value
+
+ const updatedNote = await prisma.note.upsert({
+ select: { id: true, owner: { select: { username: true } } },
+ where: { id: noteId ?? '__new_note__' },
+ create: {
+ ownerId: userId,
+ title,
+ content,
+ images: { create: newImages },
+ },
+ update: {
+ title,
+ content,
+ images: {
+ deleteMany: { id: { notIn: imageUpdates.map((i) => i.id) } },
+ updateMany: imageUpdates.map((updates) => ({
+ where: { id: updates.id },
+ data: { ...updates, id: updates.blob ? cuid() : updates.id },
+ })),
+ create: newImages,
+ },
+ },
+ })
+
+ return redirect(
+ `/users/${updatedNote.owner.username}/notes/${updatedNote.id}`,
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/users+/$username_+/__note-editor.tsx b/examples/federation/epic-stack-remote/app/routes/users+/$username_+/__note-editor.tsx
new file mode 100644
index 0000000..88f908d
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/users+/$username_+/__note-editor.tsx
@@ -0,0 +1,266 @@
+import {
+ FormProvider,
+ getFieldsetProps,
+ getFormProps,
+ getInputProps,
+ getTextareaProps,
+ useForm,
+ type FieldMetadata,
+} from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { useState } from 'react'
+import { Form } from 'react-router'
+import { z } from 'zod'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { floatingToolbarClassName } from '#app/components/floating-toolbar.tsx'
+import { ErrorList, Field, TextareaField } from '#app/components/forms.tsx'
+import { Button } from '#app/components/ui/button.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { Label } from '#app/components/ui/label.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import { Textarea } from '#app/components/ui/textarea.tsx'
+import { cn, getNoteImgSrc, useIsPending } from '#app/utils/misc.tsx'
+import { type Info } from './+types/notes.$noteId_.edit.ts'
+
+const titleMinLength = 1
+const titleMaxLength = 100
+const contentMinLength = 1
+const contentMaxLength = 10000
+
+export const MAX_UPLOAD_SIZE = 1024 * 1024 * 3 // 3MB
+
+const ImageFieldsetSchema = z.object({
+ id: z.string().optional(),
+ file: z
+ .instanceof(File)
+ .optional()
+ .refine((file) => {
+ return !file || file.size <= MAX_UPLOAD_SIZE
+ }, 'File size must be less than 3MB'),
+ altText: z.string().optional(),
+})
+
+export type ImageFieldset = z.infer
+
+export const NoteEditorSchema = z.object({
+ id: z.string().optional(),
+ title: z.string().min(titleMinLength).max(titleMaxLength),
+ content: z.string().min(contentMinLength).max(contentMaxLength),
+ images: z.array(ImageFieldsetSchema).max(5).optional(),
+})
+
+export function NoteEditor({
+ note,
+ actionData,
+}: {
+ note?: Info['loaderData']['note']
+ actionData?: Info['actionData']
+}) {
+ const isPending = useIsPending()
+
+ const [form, fields] = useForm({
+ id: 'note-editor',
+ constraint: getZodConstraint(NoteEditorSchema),
+ lastResult: actionData?.result,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: NoteEditorSchema })
+ },
+ defaultValue: {
+ ...note,
+ images: note?.images ?? [{}],
+ },
+ shouldRevalidate: 'onBlur',
+ })
+ const imageList = fields.images.getFieldList()
+
+ return (
+
+
+
+
+
+ Reset
+
+
+ Submit
+
+
+
+
+ )
+}
+
+function ImageChooser({ meta }: { meta: FieldMetadata }) {
+ const fields = meta.getFieldset()
+ const existingImage = Boolean(fields.id.initialValue)
+ const [previewImage, setPreviewImage] = useState(
+ fields.id.initialValue ? getNoteImgSrc(fields.id.initialValue) : null,
+ )
+ const [altText, setAltText] = useState(fields.altText.initialValue ?? '')
+
+ return (
+
+
+
+
+
+ {previewImage ? (
+
+
+ {existingImage ? null : (
+
+ new
+
+ )}
+
+ ) : (
+
+
+
+ )}
+ {existingImage ? (
+
+ ) : null}
+ {
+ const file = event.target.files?.[0]
+
+ if (file) {
+ const reader = new FileReader()
+ reader.onloadend = () => {
+ setPreviewImage(reader.result as string)
+ }
+ reader.readAsDataURL(file)
+ } else {
+ setPreviewImage(null)
+ }
+ }}
+ accept="image/*"
+ {...getInputProps(fields.file, { type: 'file' })}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return (
+ (
+ No note with the id "{params.noteId}" exists
+ ),
+ }}
+ />
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/users+/$username_+/notes.$noteId.tsx b/examples/federation/epic-stack-remote/app/routes/users+/$username_+/notes.$noteId.tsx
new file mode 100644
index 0000000..ce1cb02
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/users+/$username_+/notes.$noteId.tsx
@@ -0,0 +1,215 @@
+import { getFormProps, useForm } from '@conform-to/react'
+import { parseWithZod } from '@conform-to/zod'
+import { invariantResponse } from '@epic-web/invariant'
+import { formatDistanceToNow } from 'date-fns'
+import { data, Form, Link } from 'react-router'
+import { z } from 'zod'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { floatingToolbarClassName } from '#app/components/floating-toolbar.tsx'
+import { ErrorList } from '#app/components/forms.tsx'
+import { Button } from '#app/components/ui/button.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import { requireUserId } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { getNoteImgSrc, useIsPending } from '#app/utils/misc.tsx'
+import { requireUserWithPermission } from '#app/utils/permissions.server.ts'
+import { redirectWithToast } from '#app/utils/toast.server.ts'
+import { userHasPermission, useOptionalUser } from '#app/utils/user.ts'
+import { type Route, type Info } from './+types/notes.$noteId.ts'
+import { type Info as notesInfo } from './+types/notes.ts'
+
+export async function loader({ params }: Route.LoaderArgs) {
+ const note = await prisma.note.findUnique({
+ where: { id: params.noteId },
+ select: {
+ id: true,
+ title: true,
+ content: true,
+ ownerId: true,
+ updatedAt: true,
+ images: {
+ select: {
+ id: true,
+ altText: true,
+ },
+ },
+ },
+ })
+
+ invariantResponse(note, 'Not found', { status: 404 })
+
+ const date = new Date(note.updatedAt)
+ const timeAgo = formatDistanceToNow(date)
+
+ return { note, timeAgo }
+}
+
+const DeleteFormSchema = z.object({
+ intent: z.literal('delete-note'),
+ noteId: z.string(),
+})
+
+export async function action({ request }: Route.ActionArgs) {
+ const userId = await requireUserId(request)
+ const formData = await request.formData()
+ const submission = parseWithZod(formData, {
+ schema: DeleteFormSchema,
+ })
+ if (submission.status !== 'success') {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+
+ const { noteId } = submission.value
+
+ const note = await prisma.note.findFirst({
+ select: { id: true, ownerId: true, owner: { select: { username: true } } },
+ where: { id: noteId },
+ })
+ invariantResponse(note, 'Not found', { status: 404 })
+
+ const isOwner = note.ownerId === userId
+ await requireUserWithPermission(
+ request,
+ isOwner ? `delete:note:own` : `delete:note:any`,
+ )
+
+ await prisma.note.delete({ where: { id: note.id } })
+
+ return redirectWithToast(`/users/${note.owner.username}/notes`, {
+ type: 'success',
+ title: 'Success',
+ description: 'Your note has been deleted.',
+ })
+}
+
+export default function NoteRoute({
+ loaderData,
+ actionData,
+}: Route.ComponentProps) {
+ const user = useOptionalUser()
+ const isOwner = user?.id === loaderData.note.ownerId
+ const canDelete = userHasPermission(
+ user,
+ isOwner ? `delete:note:own` : `delete:note:any`,
+ )
+ const displayBar = canDelete || isOwner
+
+ return (
+
+
{loaderData.note.title}
+
+
+ {loaderData.note.images.map((image) => (
+
+
+
+
+
+ ))}
+
+
+ {loaderData.note.content}
+
+
+ {displayBar ? (
+
+
+
+ {loaderData.timeAgo} ago
+
+
+
+ {canDelete ? (
+
+ ) : null}
+
+
+
+ Edit
+
+
+
+
+
+ ) : null}
+
+ )
+}
+
+export function DeleteNote({
+ id,
+ actionData,
+}: {
+ id: string
+ actionData: Info['actionData'] | undefined
+}) {
+ const isPending = useIsPending()
+ const [form] = useForm({
+ id: 'delete-note',
+ lastResult: actionData?.result,
+ })
+
+ return (
+
+ )
+}
+
+export const meta: Route.MetaFunction = ({ data, params, matches }) => {
+ const notesMatch = matches.find(
+ (m) => m?.id === 'routes/users+/$username_+/notes',
+ ) as { data: notesInfo['loaderData'] } | undefined
+
+ const displayName = notesMatch?.data?.owner.name ?? params.username
+ const noteTitle = data?.note.title ?? 'Note'
+ const noteContentsSummary =
+ data && data.note.content.length > 100
+ ? data?.note.content.slice(0, 97) + '...'
+ : 'No content'
+ return [
+ { title: `${noteTitle} | ${displayName}'s Notes | Epic Notes` },
+ {
+ name: 'description',
+ content: noteContentsSummary,
+ },
+ ]
+}
+
+export function ErrorBoundary() {
+ return (
+ You are not allowed to do that
,
+ 404: ({ params }) => (
+ No note with the id "{params.noteId}" exists
+ ),
+ }}
+ />
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/users+/$username_+/notes.$noteId_.edit.tsx b/examples/federation/epic-stack-remote/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
new file mode 100644
index 0000000..b1ea570
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
@@ -0,0 +1,50 @@
+import { invariantResponse } from '@epic-web/invariant'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { requireUserId } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { type Route } from './+types/notes.$noteId_.edit.ts'
+import { NoteEditor } from './__note-editor.tsx'
+
+export { action } from './__note-editor.server.tsx'
+
+export async function loader({ params, request }: Route.LoaderArgs) {
+ const userId = await requireUserId(request)
+ const note = await prisma.note.findFirst({
+ select: {
+ id: true,
+ title: true,
+ content: true,
+ images: {
+ select: {
+ id: true,
+ altText: true,
+ },
+ },
+ },
+ where: {
+ id: params.noteId,
+ ownerId: userId,
+ },
+ })
+ invariantResponse(note, 'Not found', { status: 404 })
+ return { note }
+}
+
+export default function NoteEdit({
+ loaderData,
+ actionData,
+}: Route.ComponentProps) {
+ return
+}
+
+export function ErrorBoundary() {
+ return (
+ (
+ No note with the id "{params.noteId}" exists
+ ),
+ }}
+ />
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/users+/$username_+/notes.index.tsx b/examples/federation/epic-stack-remote/app/routes/users+/$username_+/notes.index.tsx
new file mode 100644
index 0000000..8379f81
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/users+/$username_+/notes.index.tsx
@@ -0,0 +1,27 @@
+import { type Route } from './+types/notes.index.ts'
+import { type Info as notesInfo } from './+types/notes.ts'
+
+export default function NotesIndexRoute() {
+ return (
+
+ )
+}
+
+export const meta: Route.MetaFunction = ({ params, matches }) => {
+ const notesMatch = matches.find(
+ (m) => m?.id === 'routes/users+/$username_+/notes',
+ ) as { data: notesInfo['loaderData'] }
+
+ const displayName = notesMatch?.data?.owner.name ?? params.username
+ const noteCount = notesMatch?.data?.owner.notes.length ?? 0
+ const notesText = noteCount === 1 ? 'note' : 'notes'
+ return [
+ { title: `${displayName}'s Notes | Epic Notes` },
+ {
+ name: 'description',
+ content: `Checkout ${displayName}'s ${noteCount} ${notesText} on Epic Notes`,
+ },
+ ]
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/users+/$username_+/notes.new.tsx b/examples/federation/epic-stack-remote/app/routes/users+/$username_+/notes.new.tsx
new file mode 100644
index 0000000..d9a24a6
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/users+/$username_+/notes.new.tsx
@@ -0,0 +1,12 @@
+import { requireUserId } from '#app/utils/auth.server.ts'
+import { type Route } from './+types/notes.new.ts'
+import { NoteEditor } from './__note-editor.tsx'
+
+export { action } from './__note-editor.server.tsx'
+
+export async function loader({ request }: Route.LoaderArgs) {
+ await requireUserId(request)
+ return {}
+}
+
+export default NoteEditor
diff --git a/examples/federation/epic-stack-remote/app/routes/users+/$username_+/notes.tsx b/examples/federation/epic-stack-remote/app/routes/users+/$username_+/notes.tsx
new file mode 100644
index 0000000..dea63ce
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/users+/$username_+/notes.tsx
@@ -0,0 +1,99 @@
+import { invariantResponse } from '@epic-web/invariant'
+import { Link, NavLink, Outlet } from 'react-router'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { cn, getUserImgSrc } from '#app/utils/misc.tsx'
+import { useOptionalUser } from '#app/utils/user.ts'
+import { type Route } from './+types/notes.ts'
+
+export async function loader({ params }: Route.LoaderArgs) {
+ const owner = await prisma.user.findFirst({
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ image: { select: { id: true } },
+ notes: { select: { id: true, title: true } },
+ },
+ where: { username: params.username },
+ })
+
+ invariantResponse(owner, 'Owner not found', { status: 404 })
+
+ return { owner }
+}
+
+export default function NotesRoute({ loaderData }: Route.ComponentProps) {
+ const user = useOptionalUser()
+ const isOwner = user?.id === loaderData.owner.id
+ const ownerDisplayName = loaderData.owner.name ?? loaderData.owner.username
+ const navLinkDefaultClassName =
+ 'line-clamp-2 block rounded-l-full py-2 pl-8 pr-6 text-base lg:text-xl'
+ return (
+
+
+
+
+
+
+
+ {ownerDisplayName}'s Notes
+
+
+
+ {isOwner ? (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ New Note
+
+
+ ) : null}
+ {loaderData.owner.notes.map((note) => (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ {note.title}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return (
+ (
+ No user with the username "{params.username}" exists
+ ),
+ }}
+ />
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/routes/users+/index.tsx b/examples/federation/epic-stack-remote/app/routes/users+/index.tsx
new file mode 100644
index 0000000..b5b01e6
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/routes/users+/index.tsx
@@ -0,0 +1,112 @@
+import { data, redirect, Link } from 'react-router'
+import { z } from 'zod'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { ErrorList } from '#app/components/forms.tsx'
+import { SearchBar } from '#app/components/search-bar.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { cn, getUserImgSrc, useDelayedIsPending } from '#app/utils/misc.tsx'
+import { type Route } from './+types/index.ts'
+
+const UserSearchResultSchema = z.object({
+ id: z.string(),
+ username: z.string(),
+ name: z.string().nullable(),
+ imageId: z.string().nullable(),
+})
+
+const UserSearchResultsSchema = z.array(UserSearchResultSchema)
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const searchTerm = new URL(request.url).searchParams.get('search')
+ if (searchTerm === '') {
+ return redirect('/users')
+ }
+
+ const like = `%${searchTerm ?? ''}%`
+ const rawUsers = await prisma.$queryRaw`
+ SELECT User.id, User.username, User.name, UserImage.id AS imageId
+ FROM User
+ LEFT JOIN UserImage ON User.id = UserImage.userId
+ WHERE User.username LIKE ${like}
+ OR User.name LIKE ${like}
+ ORDER BY (
+ SELECT Note.updatedAt
+ FROM Note
+ WHERE Note.ownerId = User.id
+ ORDER BY Note.updatedAt DESC
+ LIMIT 1
+ ) DESC
+ LIMIT 50
+ `
+
+ const result = UserSearchResultsSchema.safeParse(rawUsers)
+ if (!result.success) {
+ return data({ status: 'error', error: result.error.message } as const, {
+ status: 400,
+ })
+ }
+ return { status: 'idle', users: result.data } as const
+}
+
+export default function UsersRoute({ loaderData }: Route.ComponentProps) {
+ const isPending = useDelayedIsPending({
+ formMethod: 'GET',
+ formAction: '/users',
+ })
+
+ if (loaderData.status === 'error') {
+ console.error(loaderData.error)
+ }
+
+ return (
+
+
Epic Notes Users
+
+
+
+
+ {loaderData.status === 'idle' ? (
+ loaderData.users.length ? (
+
+ {loaderData.users.map((user) => (
+
+
+
+ {user.name ? (
+
+ {user.name}
+
+ ) : null}
+
+ {user.username}
+
+
+
+ ))}
+
+ ) : (
+ No users found
+ )
+ ) : loaderData.status === 'error' ? (
+
+ ) : null}
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return
+}
diff --git a/examples/federation/epic-stack-remote/app/styles/tailwind.css b/examples/federation/epic-stack-remote/app/styles/tailwind.css
new file mode 100644
index 0000000..bc9460f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/styles/tailwind.css
@@ -0,0 +1,80 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ /* --font-sans: here if you have one */
+ /* --font-mono: here if you got it... */
+
+ /* prefixed with foreground because it should look good on the background */
+ --foreground-destructive: 345 82.7% 40.8%;
+
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+
+ --muted: 210 40% 93%;
+ --muted-foreground: 215.4 16.3% 30%;
+
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --input-invalid: 0 84.2% 60.2%;
+
+ --primary: 222.2 47.4% 11.2%;
+ --primary-foreground: 210 40% 98%;
+
+ --secondary: 210 20% 83%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+
+ --accent: 210 40% 90%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+
+ --destructive: 0 70% 50%;
+ --destructive-foreground: 210 40% 98%;
+
+ --ring: 215 20.2% 65.1%;
+
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+
+ /* prefixed with foreground because it should look good on the background */
+ --foreground-destructive: -4 84% 60%;
+
+ --muted: 217.2 32.6% 12%;
+ --muted-foreground: 215 20.2% 65.1%;
+
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --input-invalid: 0 62.8% 30.6%;
+
+ --primary: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+
+ --secondary: 217.2 20% 24%;
+ --secondary-foreground: 210 40% 98%;
+
+ --accent: 217.2 32.6% 10%;
+ --accent-foreground: 210 40% 98%;
+
+ --destructive: 0 60% 40%;
+ --destructive-foreground: 0 85.7% 97.3%;
+
+ --ring: 217.2 32.6% 60%;
+ }
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/auth.server.ts b/examples/federation/epic-stack-remote/app/utils/auth.server.ts
new file mode 100644
index 0000000..2ef037d
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/auth.server.ts
@@ -0,0 +1,240 @@
+import { type Connection, type Password, type User } from '@prisma/client'
+import bcrypt from 'bcryptjs'
+import { redirect } from 'react-router'
+import { Authenticator } from 'remix-auth'
+import { safeRedirect } from 'remix-utils/safe-redirect'
+import { connectionSessionStorage, providers } from './connections.server.ts'
+import { prisma } from './db.server.ts'
+import { combineHeaders, downloadFile } from './misc.tsx'
+import { type ProviderUser } from './providers/provider.ts'
+import { authSessionStorage } from './session.server.ts'
+
+export const SESSION_EXPIRATION_TIME = 1000 * 60 * 60 * 24 * 30
+export const getSessionExpirationDate = () =>
+ new Date(Date.now() + SESSION_EXPIRATION_TIME)
+
+export const sessionKey = 'sessionId'
+
+export const authenticator = new Authenticator(
+ connectionSessionStorage,
+)
+
+for (const [providerName, provider] of Object.entries(providers)) {
+ authenticator.use(provider.getAuthStrategy(), providerName)
+}
+
+export async function getUserId(request: Request) {
+ const authSession = await authSessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const sessionId = authSession.get(sessionKey)
+ if (!sessionId) return null
+ const session = await prisma.session.findUnique({
+ select: { userId: true },
+ where: { id: sessionId, expirationDate: { gt: new Date() } },
+ })
+ if (!session?.userId) {
+ throw redirect('/', {
+ headers: {
+ 'set-cookie': await authSessionStorage.destroySession(authSession),
+ },
+ })
+ }
+ return session.userId
+}
+
+export async function requireUserId(
+ request: Request,
+ { redirectTo }: { redirectTo?: string | null } = {},
+) {
+ const userId = await getUserId(request)
+ if (!userId) {
+ const requestUrl = new URL(request.url)
+ redirectTo =
+ redirectTo === null
+ ? null
+ : (redirectTo ?? `${requestUrl.pathname}${requestUrl.search}`)
+ const loginParams = redirectTo ? new URLSearchParams({ redirectTo }) : null
+ const loginRedirect = ['/login', loginParams?.toString()]
+ .filter(Boolean)
+ .join('?')
+ throw redirect(loginRedirect)
+ }
+ return userId
+}
+
+export async function requireAnonymous(request: Request) {
+ const userId = await getUserId(request)
+ if (userId) {
+ throw redirect('/')
+ }
+}
+
+export async function login({
+ username,
+ password,
+}: {
+ username: User['username']
+ password: string
+}) {
+ const user = await verifyUserPassword({ username }, password)
+ if (!user) return null
+ const session = await prisma.session.create({
+ select: { id: true, expirationDate: true, userId: true },
+ data: {
+ expirationDate: getSessionExpirationDate(),
+ userId: user.id,
+ },
+ })
+ return session
+}
+
+export async function resetUserPassword({
+ username,
+ password,
+}: {
+ username: User['username']
+ password: string
+}) {
+ const hashedPassword = await getPasswordHash(password)
+ return prisma.user.update({
+ where: { username },
+ data: {
+ password: {
+ update: {
+ hash: hashedPassword,
+ },
+ },
+ },
+ })
+}
+
+export async function signup({
+ email,
+ username,
+ password,
+ name,
+}: {
+ email: User['email']
+ username: User['username']
+ name: User['name']
+ password: string
+}) {
+ const hashedPassword = await getPasswordHash(password)
+
+ const session = await prisma.session.create({
+ data: {
+ expirationDate: getSessionExpirationDate(),
+ user: {
+ create: {
+ email: email.toLowerCase(),
+ username: username.toLowerCase(),
+ name,
+ roles: { connect: { name: 'user' } },
+ password: {
+ create: {
+ hash: hashedPassword,
+ },
+ },
+ },
+ },
+ },
+ select: { id: true, expirationDate: true },
+ })
+
+ return session
+}
+
+export async function signupWithConnection({
+ email,
+ username,
+ name,
+ providerId,
+ providerName,
+ imageUrl,
+}: {
+ email: User['email']
+ username: User['username']
+ name: User['name']
+ providerId: Connection['providerId']
+ providerName: Connection['providerName']
+ imageUrl?: string
+}) {
+ const session = await prisma.session.create({
+ data: {
+ expirationDate: getSessionExpirationDate(),
+ user: {
+ create: {
+ email: email.toLowerCase(),
+ username: username.toLowerCase(),
+ name,
+ roles: { connect: { name: 'user' } },
+ connections: { create: { providerId, providerName } },
+ image: imageUrl
+ ? { create: await downloadFile(imageUrl) }
+ : undefined,
+ },
+ },
+ },
+ select: { id: true, expirationDate: true },
+ })
+
+ return session
+}
+
+export async function logout(
+ {
+ request,
+ redirectTo = '/',
+ }: {
+ request: Request
+ redirectTo?: string
+ },
+ responseInit?: ResponseInit,
+) {
+ const authSession = await authSessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const sessionId = authSession.get(sessionKey)
+ // if this fails, we still need to delete the session from the user's browser
+ // and it doesn't do any harm staying in the db anyway.
+ if (sessionId) {
+ // the .catch is important because that's what triggers the query.
+ // learn more about PrismaPromise: https://www.prisma.io/docs/orm/reference/prisma-client-reference#prismapromise-behavior
+ void prisma.session.deleteMany({ where: { id: sessionId } }).catch(() => {})
+ }
+ throw redirect(safeRedirect(redirectTo), {
+ ...responseInit,
+ headers: combineHeaders(
+ { 'set-cookie': await authSessionStorage.destroySession(authSession) },
+ responseInit?.headers,
+ ),
+ })
+}
+
+export async function getPasswordHash(password: string) {
+ const hash = await bcrypt.hash(password, 10)
+ return hash
+}
+
+export async function verifyUserPassword(
+ where: Pick | Pick,
+ password: Password['hash'],
+) {
+ const userWithPassword = await prisma.user.findUnique({
+ where,
+ select: { id: true, password: { select: { hash: true } } },
+ })
+
+ if (!userWithPassword || !userWithPassword.password) {
+ return null
+ }
+
+ const isValid = await bcrypt.compare(password, userWithPassword.password.hash)
+
+ if (!isValid) {
+ return null
+ }
+
+ return { id: userWithPassword.id }
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/cache.server.ts b/examples/federation/epic-stack-remote/app/utils/cache.server.ts
new file mode 100644
index 0000000..618f5a0
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/cache.server.ts
@@ -0,0 +1,182 @@
+import fs from 'node:fs'
+import {
+ cachified as baseCachified,
+ verboseReporter,
+ mergeReporters,
+ type CacheEntry,
+ type Cache as CachifiedCache,
+ type CachifiedOptions,
+ type Cache,
+ totalTtl,
+ type CreateReporter,
+} from '@epic-web/cachified'
+import { remember } from '@epic-web/remember'
+import Database from 'better-sqlite3'
+import { LRUCache } from 'lru-cache'
+import { z } from 'zod'
+import { updatePrimaryCacheValue } from '#app/routes/admin+/cache_.sqlite.server.ts'
+import { getInstanceInfo, getInstanceInfoSync } from './litefs.server.ts'
+import { cachifiedTimingReporter, type Timings } from './timing.server.ts'
+
+const CACHE_DATABASE_PATH = process.env.CACHE_DATABASE_PATH
+
+const cacheDb = remember('cacheDb', createDatabase)
+
+function createDatabase(tryAgain = true): Database.Database {
+ const db = new Database(CACHE_DATABASE_PATH)
+ const { currentIsPrimary } = getInstanceInfoSync()
+ if (!currentIsPrimary) return db
+
+ try {
+ // create cache table with metadata JSON column and value JSON column if it does not exist already
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS cache (
+ key TEXT PRIMARY KEY,
+ metadata TEXT,
+ value TEXT
+ )
+ `)
+ } catch (error: unknown) {
+ fs.unlinkSync(CACHE_DATABASE_PATH)
+ if (tryAgain) {
+ console.error(
+ `Error creating cache database, deleting the file at "${CACHE_DATABASE_PATH}" and trying again...`,
+ )
+ return createDatabase(false)
+ }
+ throw error
+ }
+ return db
+}
+
+const lru = remember(
+ 'lru-cache',
+ () => new LRUCache>({ max: 5000 }),
+)
+
+export const lruCache = {
+ name: 'app-memory-cache',
+ set: (key, value) => {
+ const ttl = totalTtl(value?.metadata)
+ lru.set(key, value, {
+ ttl: ttl === Infinity ? undefined : ttl,
+ start: value?.metadata?.createdTime,
+ })
+ return value
+ },
+ get: (key) => lru.get(key),
+ delete: (key) => lru.delete(key),
+} satisfies Cache
+
+const cacheEntrySchema = z.object({
+ metadata: z.object({
+ createdTime: z.number(),
+ ttl: z.number().nullable().optional(),
+ swr: z.number().nullable().optional(),
+ }),
+ value: z.unknown(),
+})
+const cacheQueryResultSchema = z.object({
+ metadata: z.string(),
+ value: z.string(),
+})
+
+export const cache: CachifiedCache = {
+ name: 'SQLite cache',
+ get(key) {
+ const result = cacheDb
+ .prepare('SELECT value, metadata FROM cache WHERE key = ?')
+ .get(key)
+ const parseResult = cacheQueryResultSchema.safeParse(result)
+ if (!parseResult.success) return null
+
+ const parsedEntry = cacheEntrySchema.safeParse({
+ metadata: JSON.parse(parseResult.data.metadata),
+ value: JSON.parse(parseResult.data.value),
+ })
+ if (!parsedEntry.success) return null
+ const { metadata, value } = parsedEntry.data
+ if (!value) return null
+ return { metadata, value }
+ },
+ async set(key, entry) {
+ const { currentIsPrimary, primaryInstance } = await getInstanceInfo()
+ if (currentIsPrimary) {
+ cacheDb
+ .prepare(
+ 'INSERT OR REPLACE INTO cache (key, value, metadata) VALUES (@key, @value, @metadata)',
+ )
+ .run({
+ key,
+ value: JSON.stringify(entry.value),
+ metadata: JSON.stringify(entry.metadata),
+ })
+ } else {
+ // fire-and-forget cache update
+ void updatePrimaryCacheValue({
+ key,
+ cacheValue: entry,
+ }).then((response) => {
+ if (!response.ok) {
+ console.error(
+ `Error updating cache value for key "${key}" on primary instance (${primaryInstance}): ${response.status} ${response.statusText}`,
+ { entry },
+ )
+ }
+ })
+ }
+ },
+ async delete(key) {
+ const { currentIsPrimary, primaryInstance } = await getInstanceInfo()
+ if (currentIsPrimary) {
+ cacheDb.prepare('DELETE FROM cache WHERE key = ?').run(key)
+ } else {
+ // fire-and-forget cache update
+ void updatePrimaryCacheValue({
+ key,
+ cacheValue: undefined,
+ }).then((response) => {
+ if (!response.ok) {
+ console.error(
+ `Error deleting cache value for key "${key}" on primary instance (${primaryInstance}): ${response.status} ${response.statusText}`,
+ )
+ }
+ })
+ }
+ },
+}
+
+export async function getAllCacheKeys(limit: number) {
+ return {
+ sqlite: cacheDb
+ .prepare('SELECT key FROM cache LIMIT ?')
+ .all(limit)
+ .map((row) => (row as { key: string }).key),
+ lru: [...lru.keys()],
+ }
+}
+
+export async function searchCacheKeys(search: string, limit: number) {
+ return {
+ sqlite: cacheDb
+ .prepare('SELECT key FROM cache WHERE key LIKE ? LIMIT ?')
+ .all(`%${search}%`, limit)
+ .map((row) => (row as { key: string }).key),
+ lru: [...lru.keys()].filter((key) => key.includes(search)),
+ }
+}
+
+export async function cachified(
+ {
+ timings,
+ ...options
+ }: CachifiedOptions & {
+ timings?: Timings
+ },
+ reporter: CreateReporter = verboseReporter(),
+): Promise {
+ return baseCachified(
+ options,
+ mergeReporters(cachifiedTimingReporter(timings), reporter),
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/client-hints.tsx b/examples/federation/epic-stack-remote/app/utils/client-hints.tsx
new file mode 100644
index 0000000..560f35a
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/client-hints.tsx
@@ -0,0 +1,56 @@
+/**
+ * This file contains utilities for using client hints for user preference which
+ * are needed by the server, but are only known by the browser.
+ */
+import { getHintUtils } from '@epic-web/client-hints'
+import {
+ clientHint as colorSchemeHint,
+ subscribeToSchemeChange,
+} from '@epic-web/client-hints/color-scheme'
+import { clientHint as timeZoneHint } from '@epic-web/client-hints/time-zone'
+import * as React from 'react'
+import { useRevalidator } from 'react-router'
+import { useOptionalRequestInfo, useRequestInfo } from './request-info.ts'
+
+const hintsUtils = getHintUtils({
+ theme: colorSchemeHint,
+ timeZone: timeZoneHint,
+ // add other hints here
+})
+
+export const { getHints } = hintsUtils
+
+/**
+ * @returns an object with the client hints and their values
+ */
+export function useHints() {
+ const requestInfo = useRequestInfo()
+ return requestInfo.hints
+}
+
+export function useOptionalHints() {
+ const requestInfo = useOptionalRequestInfo()
+ return requestInfo?.hints
+}
+
+/**
+ * @returns inline script element that checks for client hints and sets cookies
+ * if they are not set then reloads the page if any cookie was set to an
+ * inaccurate value.
+ */
+export function ClientHintCheck({ nonce }: { nonce: string }) {
+ const { revalidate } = useRevalidator()
+ React.useEffect(
+ () => subscribeToSchemeChange(() => revalidate()),
+ [revalidate],
+ )
+
+ return (
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/connections.server.ts b/examples/federation/epic-stack-remote/app/utils/connections.server.ts
new file mode 100644
index 0000000..2d87812
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/connections.server.ts
@@ -0,0 +1,33 @@
+import { createCookieSessionStorage } from 'react-router'
+import { type ProviderName } from './connections.tsx'
+import { GitHubProvider } from './providers/github.server.ts'
+import { type AuthProvider } from './providers/provider.ts'
+import { type Timings } from './timing.server.ts'
+
+export const connectionSessionStorage = createCookieSessionStorage({
+ cookie: {
+ name: 'en_connection',
+ sameSite: 'lax', // CSRF protection is advised if changing to 'none'
+ path: '/',
+ httpOnly: true,
+ maxAge: 60 * 10, // 10 minutes
+ secrets: process.env.SESSION_SECRET.split(','),
+ secure: process.env.NODE_ENV === 'production',
+ },
+})
+
+export const providers: Record = {
+ github: new GitHubProvider(),
+}
+
+export function handleMockAction(providerName: ProviderName, request: Request) {
+ return providers[providerName].handleMockAction(request)
+}
+
+export function resolveConnectionData(
+ providerName: ProviderName,
+ providerId: string,
+ options?: { timings?: Timings },
+) {
+ return providers[providerName].resolveConnectionData(providerId, options)
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/connections.tsx b/examples/federation/epic-stack-remote/app/utils/connections.tsx
new file mode 100644
index 0000000..9448b96
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/connections.tsx
@@ -0,0 +1,57 @@
+import { Form } from 'react-router'
+import { z } from 'zod'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { StatusButton } from '#app/components/ui/status-button.tsx'
+import { useIsPending } from './misc.tsx'
+
+export const GITHUB_PROVIDER_NAME = 'github'
+// to add another provider, set their name here and add it to the providerNames below
+
+export const providerNames = [GITHUB_PROVIDER_NAME] as const
+export const ProviderNameSchema = z.enum(providerNames)
+export type ProviderName = z.infer
+
+export const providerLabels: Record = {
+ [GITHUB_PROVIDER_NAME]: 'GitHub',
+} as const
+
+export const providerIcons: Record = {
+ [GITHUB_PROVIDER_NAME]: ,
+} as const
+
+export function ProviderConnectionForm({
+ redirectTo,
+ type,
+ providerName,
+}: {
+ redirectTo?: string | null
+ type: 'Connect' | 'Login' | 'Signup'
+ providerName: ProviderName
+}) {
+ const label = providerLabels[providerName]
+ const formAction = `/auth/${providerName}`
+ const isPending = useIsPending({ formAction })
+ return (
+
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/db.server.ts b/examples/federation/epic-stack-remote/app/utils/db.server.ts
new file mode 100644
index 0000000..0de9d3d
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/db.server.ts
@@ -0,0 +1,37 @@
+import { remember } from '@epic-web/remember'
+import {PrismaClient} from '@prisma/client/index'
+
+import chalk from 'chalk'
+
+export const prisma = remember('prisma', () => {
+ // NOTE: if you change anything in this function you'll need to restart
+ // the dev server to see your changes.
+
+ // Feel free to change this log threshold to something that makes sense for you
+ const logThreshold = 20
+
+ const client = new PrismaClient({
+ log: [
+ { level: 'query', emit: 'event' },
+ { level: 'error', emit: 'stdout' },
+ { level: 'warn', emit: 'stdout' },
+ ],
+ })
+ client.$on('query', async (e) => {
+ if (e.duration < logThreshold) return
+ const color =
+ e.duration < logThreshold * 1.1
+ ? 'green'
+ : e.duration < logThreshold * 1.2
+ ? 'blue'
+ : e.duration < logThreshold * 1.3
+ ? 'yellow'
+ : e.duration < logThreshold * 1.4
+ ? 'redBright'
+ : 'red'
+ const dur = chalk[color](`${e.duration}ms`)
+ console.info(`prisma:query - ${dur} - ${e.query}`)
+ })
+ void client.$connect()
+ return client
+})
diff --git a/examples/federation/epic-stack-remote/app/utils/email.server.ts b/examples/federation/epic-stack-remote/app/utils/email.server.ts
new file mode 100644
index 0000000..894acaa
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/email.server.ts
@@ -0,0 +1,97 @@
+import { render } from '@react-email/components'
+import { type ReactElement } from 'react'
+import { z } from 'zod'
+
+const resendErrorSchema = z.union([
+ z.object({
+ name: z.string(),
+ message: z.string(),
+ statusCode: z.number(),
+ }),
+ z.object({
+ name: z.literal('UnknownError'),
+ message: z.literal('Unknown Error'),
+ statusCode: z.literal(500),
+ cause: z.any(),
+ }),
+])
+type ResendError = z.infer
+
+const resendSuccessSchema = z.object({
+ id: z.string(),
+})
+
+export async function sendEmail({
+ react,
+ ...options
+}: {
+ to: string
+ subject: string
+} & (
+ | { html: string; text: string; react?: never }
+ | { react: ReactElement; html?: never; text?: never }
+)) {
+ const from = 'hello@epicstack.dev'
+
+ const email = {
+ from,
+ ...options,
+ ...(react ? await renderReactEmail(react) : null),
+ }
+ // feel free to remove this condition once you've set up resend
+ if (!process.env.RESEND_API_KEY && !process.env.MOCKS) {
+ console.error(`RESEND_API_KEY not set and we're not in mocks mode.`)
+ console.error(
+ `To send emails, set the RESEND_API_KEY environment variable.`,
+ )
+ console.error(`Would have sent the following email:`, JSON.stringify(email))
+ return {
+ status: 'success',
+ data: { id: 'mocked' },
+ } as const
+ }
+
+ const response = await fetch('https://api.resend.com/emails', {
+ method: 'POST',
+ body: JSON.stringify(email),
+ headers: {
+ Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
+ 'Content-Type': 'application/json',
+ },
+ })
+ const data = await response.json()
+ const parsedData = resendSuccessSchema.safeParse(data)
+
+ if (response.ok && parsedData.success) {
+ return {
+ status: 'success',
+ data: parsedData,
+ } as const
+ } else {
+ const parseResult = resendErrorSchema.safeParse(data)
+ if (parseResult.success) {
+ return {
+ status: 'error',
+ error: parseResult.data,
+ } as const
+ } else {
+ return {
+ status: 'error',
+ error: {
+ name: 'UnknownError',
+ message: 'Unknown Error',
+ statusCode: 500,
+ cause: data,
+ } satisfies ResendError,
+ } as const
+ }
+ }
+}
+
+async function renderReactEmail(react: ReactElement) {
+ const [html, text] = await Promise.all([
+ render(react),
+ render(react, { plainText: true }),
+ ])
+ return { html, text }
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/env.server.ts b/examples/federation/epic-stack-remote/app/utils/env.server.ts
new file mode 100644
index 0000000..e59eb46
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/env.server.ts
@@ -0,0 +1,65 @@
+import { z } from 'zod'
+
+const schema = z.object({
+ NODE_ENV: z.enum(['production', 'development', 'test'] as const),
+ DATABASE_PATH: z.string(),
+ DATABASE_URL: z.string(),
+ SESSION_SECRET: z.string(),
+ INTERNAL_COMMAND_TOKEN: z.string(),
+ HONEYPOT_SECRET: z.string(),
+ CACHE_DATABASE_PATH: z.string(),
+ // If you plan on using Sentry, uncomment this line
+ // SENTRY_DSN: z.string(),
+ // If you plan to use Resend, uncomment this line
+ // RESEND_API_KEY: z.string(),
+ // If you plan to use GitHub auth, remove the default:
+ GITHUB_CLIENT_ID: z.string().default('MOCK_GITHUB_CLIENT_ID'),
+ GITHUB_CLIENT_SECRET: z.string().default('MOCK_GITHUB_CLIENT_SECRET'),
+ GITHUB_TOKEN: z.string().default('MOCK_GITHUB_TOKEN'),
+ ALLOW_INDEXING: z.enum(['true', 'false']).optional(),
+})
+
+declare global {
+ namespace NodeJS {
+ interface ProcessEnv extends z.infer {}
+ }
+}
+
+export function init() {
+ const parsed = schema.safeParse(process.env)
+
+ if (parsed.success === false) {
+ console.error(
+ 'β Invalid environment variables:',
+ parsed.error.flatten().fieldErrors,
+ )
+
+ throw new Error('Invalid environment variables')
+ }
+}
+
+/**
+ * This is used in both `entry.server.ts` and `root.tsx` to ensure that
+ * the environment variables are set and globally available before the app is
+ * started.
+ *
+ * NOTE: Do *not* add any environment variables in here that you do not wish to
+ * be included in the client.
+ * @returns all public ENV variables
+ */
+export function getEnv() {
+ return {
+ MODE: process.env.NODE_ENV,
+ SENTRY_DSN: process.env.SENTRY_DSN,
+ ALLOW_INDEXING: process.env.ALLOW_INDEXING,
+ }
+}
+
+type ENV = ReturnType
+
+declare global {
+ var ENV: ENV
+ interface Window {
+ ENV: ENV
+ }
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/extended-theme.ts b/examples/federation/epic-stack-remote/app/utils/extended-theme.ts
new file mode 100644
index 0000000..d2ec3f1
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/extended-theme.ts
@@ -0,0 +1,102 @@
+import { type Config } from 'tailwindcss'
+
+export const extendedTheme = {
+ colors: {
+ border: 'hsl(var(--border))',
+ input: {
+ DEFAULT: 'hsl(var(--input))',
+ invalid: 'hsl(var(--input-invalid))',
+ },
+ ring: {
+ DEFAULT: 'hsl(var(--ring))',
+ invalid: 'hsl(var(--foreground-destructive))',
+ },
+ background: 'hsl(var(--background))',
+ foreground: {
+ DEFAULT: 'hsl(var(--foreground))',
+ destructive: 'hsl(var(--foreground-destructive))',
+ },
+ primary: {
+ DEFAULT: 'hsl(var(--primary))',
+ foreground: 'hsl(var(--primary-foreground))',
+ },
+ secondary: {
+ DEFAULT: 'hsl(var(--secondary))',
+ foreground: 'hsl(var(--secondary-foreground))',
+ },
+ destructive: {
+ DEFAULT: 'hsl(var(--destructive))',
+ foreground: 'hsl(var(--destructive-foreground))',
+ },
+ muted: {
+ DEFAULT: 'hsl(var(--muted))',
+ foreground: 'hsl(var(--muted-foreground))',
+ },
+ accent: {
+ DEFAULT: 'hsl(var(--accent))',
+ foreground: 'hsl(var(--accent-foreground))',
+ },
+ popover: {
+ DEFAULT: 'hsl(var(--popover))',
+ foreground: 'hsl(var(--popover-foreground))',
+ },
+ card: {
+ DEFAULT: 'hsl(var(--card))',
+ foreground: 'hsl(var(--card-foreground))',
+ },
+ },
+ borderColor: {
+ DEFAULT: 'hsl(var(--border))',
+ },
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)',
+ },
+ fontSize: {
+ // 1rem = 16px
+ /** 80px size / 84px high / bold */
+ mega: ['5rem', { lineHeight: '5.25rem', fontWeight: '700' }],
+ /** 56px size / 62px high / bold */
+ h1: ['3.5rem', { lineHeight: '3.875rem', fontWeight: '700' }],
+ /** 40px size / 48px high / bold */
+ h2: ['2.5rem', { lineHeight: '3rem', fontWeight: '700' }],
+ /** 32px size / 36px high / bold */
+ h3: ['2rem', { lineHeight: '2.25rem', fontWeight: '700' }],
+ /** 28px size / 36px high / bold */
+ h4: ['1.75rem', { lineHeight: '2.25rem', fontWeight: '700' }],
+ /** 24px size / 32px high / bold */
+ h5: ['1.5rem', { lineHeight: '2rem', fontWeight: '700' }],
+ /** 16px size / 20px high / bold */
+ h6: ['1rem', { lineHeight: '1.25rem', fontWeight: '700' }],
+
+ /** 32px size / 36px high / normal */
+ 'body-2xl': ['2rem', { lineHeight: '2.25rem' }],
+ /** 28px size / 36px high / normal */
+ 'body-xl': ['1.75rem', { lineHeight: '2.25rem' }],
+ /** 24px size / 32px high / normal */
+ 'body-lg': ['1.5rem', { lineHeight: '2rem' }],
+ /** 20px size / 28px high / normal */
+ 'body-md': ['1.25rem', { lineHeight: '1.75rem' }],
+ /** 16px size / 20px high / normal */
+ 'body-sm': ['1rem', { lineHeight: '1.25rem' }],
+ /** 14px size / 18px high / normal */
+ 'body-xs': ['0.875rem', { lineHeight: '1.125rem' }],
+ /** 12px size / 16px high / normal */
+ 'body-2xs': ['0.75rem', { lineHeight: '1rem' }],
+
+ /** 18px size / 24px high / semibold */
+ caption: ['1.125rem', { lineHeight: '1.5rem', fontWeight: '600' }],
+ /** 12px size / 16px high / bold */
+ button: ['0.75rem', { lineHeight: '1rem', fontWeight: '700' }],
+ },
+ keyframes: {
+ 'caret-blink': {
+ '0%,70%,100%': { opacity: '1' },
+ '20%,50%': { opacity: '0' },
+ },
+ },
+ animation: {
+ 'caret-blink': 'caret-blink 1.25s ease-out infinite',
+ },
+} satisfies Config['theme']
diff --git a/examples/federation/epic-stack-remote/app/utils/file-uploads.server.ts b/examples/federation/epic-stack-remote/app/utils/file-uploads.server.ts
new file mode 100644
index 0000000..4e93a7a
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/file-uploads.server.ts
@@ -0,0 +1,16 @@
+import { type FileUpload } from '@mjackson/form-data-parser'
+
+/**
+ * Handles the upload of a single file and converts it into a File object.
+ *
+ * @param {FileUpload} file - The FileUpload object to process.
+ * @returns {Promise} - A promise that resolves with the converted File object.
+ */
+export async function uploadHandler(file: FileUpload): Promise {
+ const fileContent = await file.arrayBuffer() // Get file content as an ArrayBuffer
+ const fileInstance = new File([fileContent], file.name, {
+ type: file.type,
+ lastModified: file.lastModified,
+ })
+ return fileInstance
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/headers.server.test.ts b/examples/federation/epic-stack-remote/app/utils/headers.server.test.ts
new file mode 100644
index 0000000..42b5a1a
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/headers.server.test.ts
@@ -0,0 +1,39 @@
+import { format, parse } from '@tusbar/cache-control'
+import { expect, test } from 'vitest'
+import { getConservativeCacheControl } from './headers.server.ts'
+
+test('works for basic usecase', () => {
+ const result = getConservativeCacheControl(
+ 'max-age=3600',
+ 'max-age=1800, s-maxage=600',
+ 'private, max-age=86400',
+ )
+
+ expect(result).toEqual(
+ format({
+ maxAge: 1800,
+ sharedMaxAge: 600,
+ private: true,
+ }),
+ )
+})
+test('retains boolean directive', () => {
+ const result = parse(
+ getConservativeCacheControl('private', 'no-cache,no-store'),
+ )
+
+ expect(result.private).toEqual(true)
+ expect(result.noCache).toEqual(true)
+ expect(result.noStore).toEqual(true)
+})
+test('gets smallest number directive', () => {
+ const result = parse(
+ getConservativeCacheControl(
+ 'max-age=10, s-maxage=300',
+ 'max-age=300, s-maxage=600',
+ ),
+ )
+
+ expect(result.maxAge).toEqual(10)
+ expect(result.sharedMaxAge).toEqual(300)
+})
diff --git a/examples/federation/epic-stack-remote/app/utils/headers.server.ts b/examples/federation/epic-stack-remote/app/utils/headers.server.ts
new file mode 100644
index 0000000..26f6818
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/headers.server.ts
@@ -0,0 +1,114 @@
+import { type CacheControlValue, parse, format } from '@tusbar/cache-control'
+import { type HeadersArgs } from 'react-router'
+
+/**
+ * A utility for handling route headers, merging common use-case headers.
+ *
+ * This function combines headers by:
+ * 1. Forwarding headers from the route's loader or action.
+ * 2. Inheriting headers from the parent.
+ * 3. Falling back to parent headers (if any) when headers are missing.
+ */
+export function pipeHeaders({
+ parentHeaders,
+ loaderHeaders,
+ actionHeaders,
+ errorHeaders,
+}: HeadersArgs) {
+ const headers = new Headers()
+
+ // get the one that's actually in use
+ let currentHeaders: Headers
+ if (errorHeaders !== undefined) {
+ currentHeaders = errorHeaders
+ } else if (loaderHeaders.entries().next().done) {
+ currentHeaders = actionHeaders
+ } else {
+ currentHeaders = loaderHeaders
+ }
+
+ // take in useful headers route loader/action
+ // pass this point currentHeaders can be ignored
+ const forwardHeaders = ['Cache-Control', 'Vary', 'Server-Timing']
+ for (const headerName of forwardHeaders) {
+ const header = currentHeaders.get(headerName)
+ if (header) {
+ headers.set(headerName, header)
+ }
+ }
+
+ headers.set(
+ 'Cache-Control',
+ getConservativeCacheControl(
+ parentHeaders.get('Cache-Control'),
+ headers.get('Cache-Control'),
+ ),
+ )
+
+ // append useful parent headers
+ const inheritHeaders = ['Vary', 'Server-Timing']
+ for (const headerName of inheritHeaders) {
+ const header = parentHeaders.get(headerName)
+ if (header) {
+ headers.append(headerName, header)
+ }
+ }
+
+ // fallback to parent headers if loader don't have
+ const fallbackHeaders = ['Cache-Control', 'Vary']
+ for (const headerName of fallbackHeaders) {
+ if (headers.has(headerName)) {
+ continue
+ }
+ const fallbackHeader = parentHeaders.get(headerName)
+ if (fallbackHeader) {
+ headers.set(headerName, fallbackHeader)
+ }
+ }
+
+ return headers
+}
+
+/**
+ * Given multiple Cache-Control headers, merge them and get the most conservative one.
+ */
+export function getConservativeCacheControl(
+ ...cacheControlHeaders: Array
+): string {
+ return format(
+ cacheControlHeaders
+ .filter(Boolean)
+ .map((header) => parse(header))
+ .reduce((acc, current) => {
+ for (const key in current) {
+ const directive = key as keyof Required // keyof CacheControl includes functions
+
+ const currentValue = current[directive]
+
+ switch (typeof currentValue) {
+ case 'boolean': {
+ if (currentValue) {
+ acc[directive] = true as any
+ }
+
+ break
+ }
+ case 'number': {
+ const accValue = acc[directive] as number | undefined
+
+ if (accValue === undefined) {
+ acc[directive] = currentValue as any
+ } else {
+ const result = Math.min(accValue, currentValue)
+ acc[directive] = result as any
+ }
+
+ break
+ }
+ }
+ }
+
+ return acc
+ }, {}),
+ )
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/honeypot.server.ts b/examples/federation/epic-stack-remote/app/utils/honeypot.server.ts
new file mode 100644
index 0000000..55b18a5
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/honeypot.server.ts
@@ -0,0 +1,17 @@
+import { Honeypot, SpamError } from 'remix-utils/honeypot/server'
+
+export const honeypot = new Honeypot({
+ validFromFieldName: process.env.NODE_ENV === 'test' ? null : undefined,
+ encryptionSeed: process.env.HONEYPOT_SECRET,
+})
+
+export async function checkHoneypot(formData: FormData) {
+ try {
+ await honeypot.check(formData)
+ } catch (error) {
+ if (error instanceof SpamError) {
+ throw new Response('Form not submitted properly', { status: 400 })
+ }
+ throw error
+ }
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/litefs.server.ts b/examples/federation/epic-stack-remote/app/utils/litefs.server.ts
new file mode 100644
index 0000000..0565a5b
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/litefs.server.ts
@@ -0,0 +1,10 @@
+// litefs-js should be used server-side only. It imports `fs` which results in Remix
+// including a big polyfill. So we put the import in a `.server.ts` file to avoid that
+// polyfill from being included. https://github.com/epicweb-dev/epic-stack/pull/331
+export {
+ getInstanceInfo,
+ getAllInstances,
+ getInternalInstanceDomain,
+ getInstanceInfoSync,
+} from 'litefs-js'
+export { ensurePrimary, ensureInstance } from 'litefs-js/remix.js'
diff --git a/examples/federation/epic-stack-remote/app/utils/misc.error-message.test.ts b/examples/federation/epic-stack-remote/app/utils/misc.error-message.test.ts
new file mode 100644
index 0000000..1fe4d7a
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/misc.error-message.test.ts
@@ -0,0 +1,24 @@
+import { faker } from '@faker-js/faker'
+import { expect, test } from 'vitest'
+import { consoleError } from '#tests/setup/setup-test-env.ts'
+import { getErrorMessage } from './misc.tsx'
+
+test('Error object returns message', () => {
+ const message = faker.lorem.words(2)
+ expect(getErrorMessage(new Error(message))).toBe(message)
+})
+
+test('String returns itself', () => {
+ const message = faker.lorem.words(2)
+ expect(getErrorMessage(message)).toBe(message)
+})
+
+test('undefined falls back to Unknown', () => {
+ consoleError.mockImplementation(() => {})
+ expect(getErrorMessage(undefined)).toBe('Unknown Error')
+ expect(consoleError).toHaveBeenCalledWith(
+ 'Unable to get error message for error',
+ undefined,
+ )
+ expect(consoleError).toHaveBeenCalledTimes(1)
+})
diff --git a/examples/federation/epic-stack-remote/app/utils/misc.tsx b/examples/federation/epic-stack-remote/app/utils/misc.tsx
new file mode 100644
index 0000000..0356767
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/misc.tsx
@@ -0,0 +1,290 @@
+import { clsx, type ClassValue } from 'clsx'
+import { useEffect, useMemo, useRef, useState } from 'react'
+import { useFormAction, useNavigation } from 'react-router'
+import { useSpinDelay } from 'spin-delay'
+import { extendTailwindMerge } from 'tailwind-merge'
+import { extendedTheme } from './extended-theme.ts'
+
+export function getUserImgSrc(imageId?: string | null) {
+ return imageId ? `/resources/user-images/${imageId}` : '/img/user.png'
+}
+
+export function getNoteImgSrc(imageId: string) {
+ return `/resources/note-images/${imageId}`
+}
+
+export function getErrorMessage(error: unknown) {
+ if (typeof error === 'string') return error
+ if (
+ error &&
+ typeof error === 'object' &&
+ 'message' in error &&
+ typeof error.message === 'string'
+ ) {
+ return error.message
+ }
+ console.error('Unable to get error message for error', error)
+ return 'Unknown Error'
+}
+
+function formatColors() {
+ const colors = []
+ for (const [key, color] of Object.entries(extendedTheme.colors)) {
+ if (typeof color === 'string') {
+ colors.push(key)
+ } else {
+ const colorGroup = Object.keys(color).map((subKey) =>
+ subKey === 'DEFAULT' ? '' : subKey,
+ )
+ colors.push({ [key]: colorGroup })
+ }
+ }
+ return colors
+}
+
+const customTwMerge = extendTailwindMerge({
+ extend: {
+ theme: {
+ colors: formatColors(),
+ borderRadius: Object.keys(extendedTheme.borderRadius),
+ },
+ classGroups: {
+ 'font-size': [
+ {
+ text: Object.keys(extendedTheme.fontSize),
+ },
+ ],
+ },
+ },
+})
+
+export function cn(...inputs: ClassValue[]) {
+ return customTwMerge(clsx(inputs))
+}
+
+export function getDomainUrl(request: Request) {
+ const host =
+ request.headers.get('X-Forwarded-Host') ??
+ request.headers.get('host') ??
+ new URL(request.url).host
+ const protocol = request.headers.get('X-Forwarded-Proto') ?? 'http'
+ return `${protocol}://${host}`
+}
+
+export function getReferrerRoute(request: Request) {
+ // spelling errors and whatever makes this annoyingly inconsistent
+ // in my own testing, `referer` returned the right value, but π€·ββοΈ
+ const referrer =
+ request.headers.get('referer') ??
+ request.headers.get('referrer') ??
+ request.referrer
+ const domain = getDomainUrl(request)
+ if (referrer?.startsWith(domain)) {
+ return referrer.slice(domain.length)
+ } else {
+ return '/'
+ }
+}
+
+/**
+ * Merge multiple headers objects into one (uses set so headers are overridden)
+ */
+export function mergeHeaders(
+ ...headers: Array
+) {
+ const merged = new Headers()
+ for (const header of headers) {
+ if (!header) continue
+ for (const [key, value] of new Headers(header).entries()) {
+ merged.set(key, value)
+ }
+ }
+ return merged
+}
+
+/**
+ * Combine multiple header objects into one (uses append so headers are not overridden)
+ */
+export function combineHeaders(
+ ...headers: Array
+) {
+ const combined = new Headers()
+ for (const header of headers) {
+ if (!header) continue
+ for (const [key, value] of new Headers(header).entries()) {
+ combined.append(key, value)
+ }
+ }
+ return combined
+}
+
+/**
+ * Combine multiple response init objects into one (uses combineHeaders)
+ */
+export function combineResponseInits(
+ ...responseInits: Array
+) {
+ let combined: ResponseInit = {}
+ for (const responseInit of responseInits) {
+ combined = {
+ ...responseInit,
+ headers: combineHeaders(combined.headers, responseInit?.headers),
+ }
+ }
+ return combined
+}
+
+/**
+ * Returns true if the current navigation is submitting the current route's
+ * form. Defaults to the current route's form action and method POST.
+ *
+ * Defaults state to 'non-idle'
+ *
+ * NOTE: the default formAction will include query params, but the
+ * navigation.formAction will not, so don't use the default formAction if you
+ * want to know if a form is submitting without specific query params.
+ */
+export function useIsPending({
+ formAction,
+ formMethod = 'POST',
+ state = 'non-idle',
+}: {
+ formAction?: string
+ formMethod?: 'POST' | 'GET' | 'PUT' | 'PATCH' | 'DELETE'
+ state?: 'submitting' | 'loading' | 'non-idle'
+} = {}) {
+ const contextualFormAction = useFormAction()
+ const navigation = useNavigation()
+ const isPendingState =
+ state === 'non-idle'
+ ? navigation.state !== 'idle'
+ : navigation.state === state
+ return (
+ isPendingState &&
+ navigation.formAction === (formAction ?? contextualFormAction) &&
+ navigation.formMethod === formMethod
+ )
+}
+
+/**
+ * This combines useSpinDelay (from https://npm.im/spin-delay) and useIsPending
+ * from our own utilities to give you a nice way to show a loading spinner for
+ * a minimum amount of time, even if the request finishes right after the delay.
+ *
+ * This avoids a flash of loading state regardless of how fast or slow the
+ * request is.
+ */
+export function useDelayedIsPending({
+ formAction,
+ formMethod,
+ delay = 400,
+ minDuration = 300,
+}: Parameters[0] &
+ Parameters[1] = {}) {
+ const isPending = useIsPending({ formAction, formMethod })
+ const delayedIsPending = useSpinDelay(isPending, {
+ delay,
+ minDuration,
+ })
+ return delayedIsPending
+}
+
+function callAll>(
+ ...fns: Array<((...args: Args) => unknown) | undefined>
+) {
+ return (...args: Args) => fns.forEach((fn) => fn?.(...args))
+}
+
+/**
+ * Use this hook with a button and it will make it so the first click sets a
+ * `doubleCheck` state to true, and the second click will actually trigger the
+ * `onClick` handler. This allows you to have a button that can be like a
+ * "are you sure?" experience for the user before doing destructive operations.
+ */
+export function useDoubleCheck() {
+ const [doubleCheck, setDoubleCheck] = useState(false)
+
+ function getButtonProps(
+ props?: React.ButtonHTMLAttributes,
+ ) {
+ const onBlur: React.ButtonHTMLAttributes['onBlur'] =
+ () => setDoubleCheck(false)
+
+ const onClick: React.ButtonHTMLAttributes['onClick'] =
+ doubleCheck
+ ? undefined
+ : (e) => {
+ e.preventDefault()
+ setDoubleCheck(true)
+ }
+
+ const onKeyUp: React.ButtonHTMLAttributes['onKeyUp'] = (
+ e,
+ ) => {
+ if (e.key === 'Escape') {
+ setDoubleCheck(false)
+ }
+ }
+
+ return {
+ ...props,
+ onBlur: callAll(onBlur, props?.onBlur),
+ onClick: callAll(onClick, props?.onClick),
+ onKeyUp: callAll(onKeyUp, props?.onKeyUp),
+ }
+ }
+
+ return { doubleCheck, getButtonProps }
+}
+
+/**
+ * Simple debounce implementation
+ */
+function debounce) => void>(
+ fn: Callback,
+ delay: number,
+) {
+ let timer: ReturnType | null = null
+ return (...args: Parameters) => {
+ if (timer) clearTimeout(timer)
+ timer = setTimeout(() => {
+ fn(...args)
+ }, delay)
+ }
+}
+
+/**
+ * Debounce a callback function
+ */
+export function useDebounce<
+ Callback extends (...args: Parameters) => ReturnType,
+>(callback: Callback, delay: number) {
+ const callbackRef = useRef(callback)
+ useEffect(() => {
+ callbackRef.current = callback
+ })
+ return useMemo(
+ () =>
+ debounce(
+ (...args: Parameters) => callbackRef.current(...args),
+ delay,
+ ),
+ [delay],
+ )
+}
+
+export async function downloadFile(url: string, retries: number = 0) {
+ const MAX_RETRIES = 3
+ try {
+ const response = await fetch(url)
+ if (!response.ok) {
+ throw new Error(`Failed to fetch image with status ${response.status}`)
+ }
+ const contentType = response.headers.get('content-type') ?? 'image/jpg'
+ const blob = Buffer.from(await response.arrayBuffer())
+ return { contentType, blob }
+ } catch (e) {
+ if (retries > MAX_RETRIES) throw e
+ return downloadFile(url, retries + 1)
+ }
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/misc.use-double-check.test.tsx b/examples/federation/epic-stack-remote/app/utils/misc.use-double-check.test.tsx
new file mode 100644
index 0000000..4adfa59
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/misc.use-double-check.test.tsx
@@ -0,0 +1,83 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { render, screen } from '@testing-library/react'
+import { userEvent } from '@testing-library/user-event'
+import { useState } from 'react'
+import { expect, test } from 'vitest'
+import { useDoubleCheck } from './misc.tsx'
+
+function TestComponent() {
+ const [defaultPrevented, setDefaultPrevented] = useState<
+ 'idle' | 'no' | 'yes'
+ >('idle')
+ const dc = useDoubleCheck()
+ return (
+
+ Default Prevented: {defaultPrevented}
+
+ setDefaultPrevented(e.defaultPrevented ? 'yes' : 'no'),
+ })}
+ >
+ {dc.doubleCheck ? 'You sure?' : 'Click me'}
+
+
+ )
+}
+
+test('prevents default on the first click, and does not on the second', async () => {
+ const user = userEvent.setup()
+ render( )
+
+ const status = screen.getByRole('status')
+ const button = screen.getByRole('button')
+
+ expect(status).toHaveTextContent('Default Prevented: idle')
+ expect(button).toHaveTextContent('Click me')
+
+ await user.click(button)
+ expect(button).toHaveTextContent('You sure?')
+ expect(status).toHaveTextContent('Default Prevented: yes')
+
+ await user.click(button)
+ expect(button).toHaveTextContent('You sure?')
+ expect(status).toHaveTextContent('Default Prevented: no')
+})
+
+test('blurring the button starts things over', async () => {
+ const user = userEvent.setup()
+ render( )
+
+ const status = screen.getByRole('status')
+ const button = screen.getByRole('button')
+
+ await user.click(button)
+ expect(button).toHaveTextContent('You sure?')
+ expect(status).toHaveTextContent('Default Prevented: yes')
+
+ await user.click(document.body)
+ // button goes back to click me
+ expect(button).toHaveTextContent('Click me')
+ // our callback wasn't called, so the status doesn't change
+ expect(status).toHaveTextContent('Default Prevented: yes')
+})
+
+test('hitting "escape" on the input starts things over', async () => {
+ const user = userEvent.setup()
+ render( )
+
+ const status = screen.getByRole('status')
+ const button = screen.getByRole('button')
+
+ await user.click(button)
+ expect(button).toHaveTextContent('You sure?')
+ expect(status).toHaveTextContent('Default Prevented: yes')
+
+ await user.keyboard('{Escape}')
+ // button goes back to click me
+ expect(button).toHaveTextContent('Click me')
+ // our callback wasn't called, so the status doesn't change
+ expect(status).toHaveTextContent('Default Prevented: yes')
+})
diff --git a/examples/federation/epic-stack-remote/app/utils/monitoring.client.tsx b/examples/federation/epic-stack-remote/app/utils/monitoring.client.tsx
new file mode 100644
index 0000000..587ffaf
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/monitoring.client.tsx
@@ -0,0 +1,49 @@
+import * as Sentry from '@sentry/react'
+import React from 'react'
+import {
+ createRoutesFromChildren,
+ matchRoutes,
+ useLocation,
+ useNavigationType,
+} from 'react-router'
+
+export function init() {
+ Sentry.init({
+ dsn: ENV.SENTRY_DSN,
+ environment: ENV.MODE,
+ beforeSend(event) {
+ if (event.request?.url) {
+ const url = new URL(event.request.url)
+ if (
+ url.protocol === 'chrome-extension:' ||
+ url.protocol === 'moz-extension:'
+ ) {
+ // This error is from a browser extension, ignore it
+ return null
+ }
+ }
+ return event
+ },
+ integrations: [
+ Sentry.replayIntegration(),
+ Sentry.browserProfilingIntegration(),
+ Sentry.reactRouterV7BrowserTracingIntegration({
+ useEffect: React.useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
+ ],
+
+ // Set tracesSampleRate to 1.0 to capture 100%
+ // of transactions for performance monitoring.
+ // We recommend adjusting this value in production
+ tracesSampleRate: 1.0,
+
+ // Capture Replay for 10% of all sessions,
+ // plus for 100% of sessions with an error
+ replaysSessionSampleRate: 0.1,
+ replaysOnErrorSampleRate: 1.0,
+ })
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/nonce-provider.ts b/examples/federation/epic-stack-remote/app/utils/nonce-provider.ts
new file mode 100644
index 0000000..41bb980
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/nonce-provider.ts
@@ -0,0 +1,5 @@
+import * as React from 'react'
+
+export const NonceContext = React.createContext('')
+export const NonceProvider = NonceContext.Provider
+export const useNonce = () => React.useContext(NonceContext)
diff --git a/examples/federation/epic-stack-remote/app/utils/permissions.server.ts b/examples/federation/epic-stack-remote/app/utils/permissions.server.ts
new file mode 100644
index 0000000..1c1d92f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/permissions.server.ts
@@ -0,0 +1,60 @@
+import { data } from 'react-router'
+import { requireUserId } from './auth.server.ts'
+import { prisma } from './db.server.ts'
+import { type PermissionString, parsePermissionString } from './user.ts'
+
+export async function requireUserWithPermission(
+ request: Request,
+ permission: PermissionString,
+) {
+ const userId = await requireUserId(request)
+ const permissionData = parsePermissionString(permission)
+ const user = await prisma.user.findFirst({
+ select: { id: true },
+ where: {
+ id: userId,
+ roles: {
+ some: {
+ permissions: {
+ some: {
+ ...permissionData,
+ access: permissionData.access
+ ? { in: permissionData.access }
+ : undefined,
+ },
+ },
+ },
+ },
+ },
+ })
+ if (!user) {
+ throw data(
+ {
+ error: 'Unauthorized',
+ requiredPermission: permissionData,
+ message: `Unauthorized: required permissions: ${permission}`,
+ },
+ { status: 403 },
+ )
+ }
+ return user.id
+}
+
+export async function requireUserWithRole(request: Request, name: string) {
+ const userId = await requireUserId(request)
+ const user = await prisma.user.findFirst({
+ select: { id: true },
+ where: { id: userId, roles: { some: { name } } },
+ })
+ if (!user) {
+ throw data(
+ {
+ error: 'Unauthorized',
+ requiredRole: name,
+ message: `Unauthorized: required role: ${name}`,
+ },
+ { status: 403 },
+ )
+ }
+ return user.id
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/providers/constants.ts b/examples/federation/epic-stack-remote/app/utils/providers/constants.ts
new file mode 100644
index 0000000..b8737a0
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/providers/constants.ts
@@ -0,0 +1,3 @@
+export const MOCK_CODE_GITHUB = 'MOCK_CODE_GITHUB_KODY'
+
+export const MOCK_CODE_GITHUB_HEADER = 'x-mock-code-github'
diff --git a/examples/federation/epic-stack-remote/app/utils/providers/github.server.ts b/examples/federation/epic-stack-remote/app/utils/providers/github.server.ts
new file mode 100644
index 0000000..054719b
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/providers/github.server.ts
@@ -0,0 +1,106 @@
+import { createId as cuid } from '@paralleldrive/cuid2'
+import { redirect } from 'react-router'
+import { GitHubStrategy } from 'remix-auth-github'
+import { z } from 'zod'
+import { cache, cachified } from '../cache.server.ts'
+import { connectionSessionStorage } from '../connections.server.ts'
+import { type Timings } from '../timing.server.ts'
+import { MOCK_CODE_GITHUB_HEADER, MOCK_CODE_GITHUB } from './constants.ts'
+import { type AuthProvider } from './provider.ts'
+
+const GitHubUserSchema = z.object({ login: z.string() })
+const GitHubUserParseResult = z
+ .object({
+ success: z.literal(true),
+ data: GitHubUserSchema,
+ })
+ .or(
+ z.object({
+ success: z.literal(false),
+ }),
+ )
+
+const shouldMock =
+ process.env.GITHUB_CLIENT_ID?.startsWith('MOCK_') ||
+ process.env.NODE_ENV === 'test'
+
+export class GitHubProvider implements AuthProvider {
+ getAuthStrategy() {
+ return new GitHubStrategy(
+ {
+ clientID: process.env.GITHUB_CLIENT_ID,
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
+ callbackURL: '/auth/github/callback',
+ },
+ async ({ profile }) => {
+ const email = profile.emails[0]?.value.trim().toLowerCase()
+ if (!email) {
+ throw new Error('Email not found')
+ }
+ const username = profile.displayName
+ const imageUrl = profile.photos[0]?.value
+ return {
+ email,
+ id: profile.id,
+ username,
+ name: profile.name.givenName,
+ imageUrl,
+ }
+ },
+ )
+ }
+
+ async resolveConnectionData(
+ providerId: string,
+ { timings }: { timings?: Timings } = {},
+ ) {
+ const result = await cachified({
+ key: `connection-data:github:${providerId}`,
+ cache,
+ timings,
+ ttl: 1000 * 60,
+ swr: 1000 * 60 * 60 * 24 * 7,
+ async getFreshValue(context) {
+ const response = await fetch(
+ `https://api.github.com/user/${providerId}`,
+ { headers: { Authorization: `token ${process.env.GITHUB_TOKEN}` } },
+ )
+ const rawJson = await response.json()
+ const result = GitHubUserSchema.safeParse(rawJson)
+ if (!result.success) {
+ // if it was unsuccessful, then we should kick it out of the cache
+ // asap and try again.
+ context.metadata.ttl = 0
+ }
+ return result
+ },
+ checkValue: GitHubUserParseResult,
+ })
+ return {
+ displayName: result.success ? result.data.login : 'Unknown',
+ link: result.success ? `https://github.com/${result.data.login}` : null,
+ } as const
+ }
+
+ async handleMockAction(request: Request) {
+ if (!shouldMock) return
+
+ const connectionSession = await connectionSessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const state = cuid()
+ connectionSession.set('oauth2:state', state)
+
+ // allows us to inject a code when running e2e tests,
+ // but falls back to a pre-defined π¨ constant
+ const code =
+ request.headers.get(MOCK_CODE_GITHUB_HEADER) || MOCK_CODE_GITHUB
+ const searchParams = new URLSearchParams({ code, state })
+ throw redirect(`/auth/github/callback?${searchParams}`, {
+ headers: {
+ 'set-cookie':
+ await connectionSessionStorage.commitSession(connectionSession),
+ },
+ })
+ }
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/providers/provider.ts b/examples/federation/epic-stack-remote/app/utils/providers/provider.ts
new file mode 100644
index 0000000..834de1f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/providers/provider.ts
@@ -0,0 +1,28 @@
+import { type Strategy } from 'remix-auth'
+import { type Timings } from '../timing.server.ts'
+
+// Define a user type for cleaner typing
+export type ProviderUser = {
+ id: string
+ email: string
+ username?: string
+ name?: string
+ imageUrl?: string
+}
+
+export interface AuthProvider {
+ getAuthStrategy(): Strategy
+ handleMockAction(request: Request): Promise
+ resolveConnectionData(
+ providerId: string,
+ options?: { timings?: Timings },
+ ): Promise<{
+ displayName: string
+ link?: string | null
+ }>
+}
+
+export const normalizeEmail = (s: string) => s.toLowerCase()
+
+export const normalizeUsername = (s: string) =>
+ s.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase()
diff --git a/examples/federation/epic-stack-remote/app/utils/redirect-cookie.server.ts b/examples/federation/epic-stack-remote/app/utils/redirect-cookie.server.ts
new file mode 100644
index 0000000..0008537
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/redirect-cookie.server.ts
@@ -0,0 +1,17 @@
+import * as cookie from 'cookie'
+
+const key = 'redirectTo'
+export const destroyRedirectToHeader = cookie.serialize(key, '', { maxAge: -1 })
+
+export function getRedirectCookieHeader(redirectTo?: string) {
+ return redirectTo && redirectTo !== '/'
+ ? cookie.serialize(key, redirectTo, { maxAge: 60 * 10 })
+ : null
+}
+
+export function getRedirectCookieValue(request: Request) {
+ const rawCookie = request.headers.get('cookie')
+ const parsedCookies = rawCookie ? cookie.parse(rawCookie) : {}
+ const redirectTo = parsedCookies[key]
+ return redirectTo || null
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/request-info.ts b/examples/federation/epic-stack-remote/app/utils/request-info.ts
new file mode 100644
index 0000000..ec4236c
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/request-info.ts
@@ -0,0 +1,19 @@
+import { invariant } from '@epic-web/invariant'
+import { useRouteLoaderData } from 'react-router'
+import { type loader as rootLoader } from '#app/root.tsx'
+
+/**
+ * @returns the request info from the root loader (throws an error if it does not exist)
+ */
+export function useRequestInfo() {
+ const maybeRequestInfo = useOptionalRequestInfo()
+ invariant(maybeRequestInfo, 'No requestInfo found in root loader')
+
+ return maybeRequestInfo
+}
+
+export function useOptionalRequestInfo() {
+ const data = useRouteLoaderData('root')
+
+ return data?.requestInfo
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/session.server.ts b/examples/federation/epic-stack-remote/app/utils/session.server.ts
new file mode 100644
index 0000000..5d9fd32
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/session.server.ts
@@ -0,0 +1,38 @@
+import { createCookieSessionStorage } from 'react-router'
+
+export const authSessionStorage = createCookieSessionStorage({
+ cookie: {
+ name: 'en_session',
+ sameSite: 'lax', // CSRF protection is advised if changing to 'none'
+ path: '/',
+ httpOnly: true,
+ secrets: process.env.SESSION_SECRET.split(','),
+ secure: process.env.NODE_ENV === 'production',
+ },
+})
+
+// we have to do this because every time you commit the session you overwrite it
+// so we store the expiration time in the cookie and reset it every time we commit
+const originalCommitSession = authSessionStorage.commitSession
+
+Object.defineProperty(authSessionStorage, 'commitSession', {
+ value: async function commitSession(
+ ...args: Parameters
+ ) {
+ const [session, options] = args
+ if (options?.expires) {
+ session.set('expires', options.expires)
+ }
+ if (options?.maxAge) {
+ session.set('expires', new Date(Date.now() + options.maxAge * 1000))
+ }
+ const expires = session.has('expires')
+ ? new Date(session.get('expires'))
+ : undefined
+ const setCookieHeader = await originalCommitSession(session, {
+ ...options,
+ expires,
+ })
+ return setCookieHeader
+ },
+})
diff --git a/examples/federation/epic-stack-remote/app/utils/theme.server.ts b/examples/federation/epic-stack-remote/app/utils/theme.server.ts
new file mode 100644
index 0000000..1d60cbc
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/theme.server.ts
@@ -0,0 +1,19 @@
+import * as cookie from 'cookie'
+
+const cookieName = 'en_theme'
+export type Theme = 'light' | 'dark'
+
+export function getTheme(request: Request): Theme | null {
+ const cookieHeader = request.headers.get('cookie')
+ const parsed = cookieHeader ? cookie.parse(cookieHeader)[cookieName] : 'light'
+ if (parsed === 'light' || parsed === 'dark') return parsed
+ return null
+}
+
+export function setTheme(theme: Theme | 'system') {
+ if (theme === 'system') {
+ return cookie.serialize(cookieName, '', { path: '/', maxAge: -1 })
+ } else {
+ return cookie.serialize(cookieName, theme, { path: '/', maxAge: 31536000 })
+ }
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/timing.server.ts b/examples/federation/epic-stack-remote/app/utils/timing.server.ts
new file mode 100644
index 0000000..3904647
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/timing.server.ts
@@ -0,0 +1,121 @@
+import { type CreateReporter } from '@epic-web/cachified'
+
+export type Timings = Record<
+ string,
+ Array<
+ { desc?: string } & (
+ | { time: number; start?: never }
+ | { time?: never; start: number }
+ )
+ >
+>
+
+export function makeTimings(type: string, desc?: string) {
+ const timings: Timings = {
+ [type]: [{ desc, start: performance.now() }],
+ }
+ Object.defineProperty(timings, 'toString', {
+ value: function () {
+ return getServerTimeHeader(timings)
+ },
+ enumerable: false,
+ })
+ return timings
+}
+
+function createTimer(type: string, desc?: string) {
+ const start = performance.now()
+ return {
+ end(timings: Timings) {
+ let timingType = timings[type]
+
+ if (!timingType) {
+ timingType = timings[type] = []
+ }
+ timingType.push({ desc, time: performance.now() - start })
+ },
+ }
+}
+
+export async function time(
+ fn: Promise | (() => ReturnType | Promise),
+ {
+ type,
+ desc,
+ timings,
+ }: {
+ type: string
+ desc?: string
+ timings?: Timings
+ },
+): Promise {
+ const timer = createTimer(type, desc)
+ const promise = typeof fn === 'function' ? fn() : fn
+ if (!timings) return promise
+
+ const result = await promise
+
+ timer.end(timings)
+ return result
+}
+
+export function getServerTimeHeader(timings?: Timings) {
+ if (!timings) return ''
+ return Object.entries(timings)
+ .map(([key, timingInfos]) => {
+ const dur = timingInfos
+ .reduce((acc, timingInfo) => {
+ const time = timingInfo.time ?? performance.now() - timingInfo.start
+ return acc + time
+ }, 0)
+ .toFixed(1)
+ const desc = timingInfos
+ .map((t) => t.desc)
+ .filter(Boolean)
+ .join(' & ')
+ return [
+ key.replaceAll(/(:| |@|=|;|,|\/|\\)/g, '_'),
+ desc ? `desc=${JSON.stringify(desc)}` : null,
+ `dur=${dur}`,
+ ]
+ .filter(Boolean)
+ .join(';')
+ })
+ .join(',')
+}
+
+export function combineServerTimings(headers1: Headers, headers2: Headers) {
+ const newHeaders = new Headers(headers1)
+ newHeaders.append('Server-Timing', headers2.get('Server-Timing') ?? '')
+ return newHeaders.get('Server-Timing') ?? ''
+}
+
+export function cachifiedTimingReporter(
+ timings?: Timings,
+): undefined | CreateReporter {
+ if (!timings) return
+
+ return ({ key }) => {
+ const cacheRetrievalTimer = createTimer(
+ `cache:${key}`,
+ `${key} cache retrieval`,
+ )
+ let getFreshValueTimer: ReturnType | undefined
+ return (event) => {
+ switch (event.name) {
+ case 'getFreshValueStart':
+ getFreshValueTimer = createTimer(
+ `getFreshValue:${key}`,
+ `request forced to wait for a fresh ${key} value`,
+ )
+ break
+ case 'getFreshValueSuccess':
+ getFreshValueTimer?.end(timings)
+ break
+ case 'done':
+ cacheRetrievalTimer.end(timings)
+ break
+ }
+ }
+ }
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/toast.server.ts b/examples/federation/epic-stack-remote/app/utils/toast.server.ts
new file mode 100644
index 0000000..b46fefa
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/toast.server.ts
@@ -0,0 +1,62 @@
+import { createId as cuid } from '@paralleldrive/cuid2'
+import { createCookieSessionStorage, redirect } from 'react-router'
+import { z } from 'zod'
+import { combineHeaders } from './misc.tsx'
+
+export const toastKey = 'toast'
+
+const ToastSchema = z.object({
+ description: z.string(),
+ id: z.string().default(() => cuid()),
+ title: z.string().optional(),
+ type: z.enum(['message', 'success', 'error']).default('message'),
+})
+
+export type Toast = z.infer
+export type ToastInput = z.input
+
+export const toastSessionStorage = createCookieSessionStorage({
+ cookie: {
+ name: 'en_toast',
+ sameSite: 'lax',
+ path: '/',
+ httpOnly: true,
+ secrets: process.env.SESSION_SECRET.split(','),
+ secure: process.env.NODE_ENV === 'production',
+ },
+})
+
+export async function redirectWithToast(
+ url: string,
+ toast: ToastInput,
+ init?: ResponseInit,
+) {
+ return redirect(url, {
+ ...init,
+ headers: combineHeaders(init?.headers, await createToastHeaders(toast)),
+ })
+}
+
+export async function createToastHeaders(toastInput: ToastInput) {
+ const session = await toastSessionStorage.getSession()
+ const toast = ToastSchema.parse(toastInput)
+ session.flash(toastKey, toast)
+ const cookie = await toastSessionStorage.commitSession(session)
+ return new Headers({ 'set-cookie': cookie })
+}
+
+export async function getToast(request: Request) {
+ const session = await toastSessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const result = ToastSchema.safeParse(session.get(toastKey))
+ const toast = result.success ? result.data : null
+ return {
+ toast,
+ headers: toast
+ ? new Headers({
+ 'set-cookie': await toastSessionStorage.destroySession(session),
+ })
+ : null,
+ }
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/totp.server.ts b/examples/federation/epic-stack-remote/app/utils/totp.server.ts
new file mode 100644
index 0000000..bc5ff26
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/totp.server.ts
@@ -0,0 +1,3 @@
+// @epic-web/totp should be used server-side only. It imports `Crypto` which results in Remix
+// including a big polyfill. So we put the import in a `.server.ts` file to avoid that
+export * from '@epic-web/totp'
diff --git a/examples/federation/epic-stack-remote/app/utils/user-validation.ts b/examples/federation/epic-stack-remote/app/utils/user-validation.ts
new file mode 100644
index 0000000..3a2a775
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/user-validation.ts
@@ -0,0 +1,48 @@
+import { z } from 'zod'
+
+export const USERNAME_MIN_LENGTH = 3
+export const USERNAME_MAX_LENGTH = 20
+
+export const UsernameSchema = z
+ .string({ required_error: 'Username is required' })
+ .min(USERNAME_MIN_LENGTH, { message: 'Username is too short' })
+ .max(USERNAME_MAX_LENGTH, { message: 'Username is too long' })
+ .regex(/^[a-zA-Z0-9_]+$/, {
+ message: 'Username can only include letters, numbers, and underscores',
+ })
+ // users can type the username in any case, but we store it in lowercase
+ .transform((value) => value.toLowerCase())
+
+export const PasswordSchema = z
+ .string({ required_error: 'Password is required' })
+ .min(6, { message: 'Password is too short' })
+ // NOTE: bcrypt has a limit of 72 bytes (which should be plenty long)
+ // https://github.com/epicweb-dev/epic-stack/issues/918
+ .refine((val) => new TextEncoder().encode(val).length <= 72, {
+ message: 'Password is too long',
+ })
+
+export const NameSchema = z
+ .string({ required_error: 'Name is required' })
+ .min(3, { message: 'Name is too short' })
+ .max(40, { message: 'Name is too long' })
+
+export const EmailSchema = z
+ .string({ required_error: 'Email is required' })
+ .email({ message: 'Email is invalid' })
+ .min(3, { message: 'Email is too short' })
+ .max(100, { message: 'Email is too long' })
+ // users can type the email in any case, but we store it in lowercase
+ .transform((value) => value.toLowerCase())
+
+export const PasswordAndConfirmPasswordSchema = z
+ .object({ password: PasswordSchema, confirmPassword: PasswordSchema })
+ .superRefine(({ confirmPassword, password }, ctx) => {
+ if (confirmPassword !== password) {
+ ctx.addIssue({
+ path: ['confirmPassword'],
+ code: 'custom',
+ message: 'The passwords must match',
+ })
+ }
+ })
diff --git a/examples/federation/epic-stack-remote/app/utils/user.ts b/examples/federation/epic-stack-remote/app/utils/user.ts
new file mode 100644
index 0000000..30534b9
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/user.ts
@@ -0,0 +1,70 @@
+import { useRouteLoaderData } from 'react-router'
+import { type loader as rootLoader } from '#app/root.tsx'
+
+function isUser(
+ user: any,
+): user is Awaited>['data']['user'] {
+ return user && typeof user === 'object' && typeof user.id === 'string'
+}
+
+export function useOptionalUser() {
+ const data = useRouteLoaderData('root')
+ if (!data || !isUser(data.user)) {
+ return undefined
+ }
+ return data.user
+}
+
+export function useUser() {
+ const maybeUser = useOptionalUser()
+ if (!maybeUser) {
+ throw new Error(
+ 'No user found in root loader, but user is required by useUser. If user is optional, try useOptionalUser instead.',
+ )
+ }
+ return maybeUser
+}
+
+type Action = 'create' | 'read' | 'update' | 'delete'
+type Entity = 'user' | 'note'
+type Access = 'own' | 'any' | 'own,any' | 'any,own'
+export type PermissionString =
+ | `${Action}:${Entity}`
+ | `${Action}:${Entity}:${Access}`
+
+export function parsePermissionString(permissionString: PermissionString) {
+ const [action, entity, access] = permissionString.split(':') as [
+ Action,
+ Entity,
+ Access | undefined,
+ ]
+ return {
+ action,
+ entity,
+ access: access ? (access.split(',') as Array) : undefined,
+ }
+}
+
+export function userHasPermission(
+ user: Pick, 'roles'> | null | undefined,
+ permission: PermissionString,
+) {
+ if (!user) return false
+ const { action, entity, access } = parsePermissionString(permission)
+ return user.roles.some((role) =>
+ role.permissions.some(
+ (permission) =>
+ permission.entity === entity &&
+ permission.action === action &&
+ (!access || access.includes(permission.access)),
+ ),
+ )
+}
+
+export function userHasRole(
+ user: Pick, 'roles'> | null,
+ role: string,
+) {
+ if (!user) return false
+ return user.roles.some((r) => r.name === role)
+}
diff --git a/examples/federation/epic-stack-remote/app/utils/verification.server.ts b/examples/federation/epic-stack-remote/app/utils/verification.server.ts
new file mode 100644
index 0000000..1099f7c
--- /dev/null
+++ b/examples/federation/epic-stack-remote/app/utils/verification.server.ts
@@ -0,0 +1,13 @@
+import { createCookieSessionStorage } from 'react-router'
+
+export const verifySessionStorage = createCookieSessionStorage({
+ cookie: {
+ name: 'en_verification',
+ sameSite: 'lax', // CSRF protection is advised if changing to 'none'
+ path: '/',
+ httpOnly: true,
+ maxAge: 60 * 10, // 10 minutes
+ secrets: process.env.SESSION_SECRET.split(','),
+ secure: process.env.NODE_ENV === 'production',
+ },
+})
diff --git a/examples/federation/epic-stack-remote/components.json b/examples/federation/epic-stack-remote/components.json
new file mode 100644
index 0000000..342acbf
--- /dev/null
+++ b/examples/federation/epic-stack-remote/components.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": false,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "app/styles/tailwind.css",
+ "baseColor": "slate",
+ "cssVariables": true
+ },
+ "aliases": {
+ "components": "#app/components",
+ "utils": "#app/utils/misc.tsx",
+ "ui": "#app/components/ui"
+ }
+}
diff --git a/examples/federation/epic-stack-remote/docs/README.md b/examples/federation/epic-stack-remote/docs/README.md
new file mode 100644
index 0000000..e330723
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/README.md
@@ -0,0 +1,35 @@
+# Epic Stack Documentation
+
+The goal of The Epic Stack is to provide solid opinions for teams to hit the
+ground running on their web applications.
+
+We recommend you watch Kent's introduction to the Epic Stack to get an
+understanding of the "why" behind the Stack:
+
+[](https://www.epicweb.dev/talks/the-epic-stack)
+
+More of a reader? Read [the announcement post](https://epicweb.dev/epic-stack)
+or
+[an AI generated summary of the video](https://www.summarize.tech/www.youtube.com/watch?v=yMK5SVRASxM).
+
+This stack is still under active development. Documentation will rapidly improve
+in the coming weeks. Stay tuned!
+
+# Top Pages
+
+- [Getting Started](./getting-started.md) - Instructions for how to get started
+ with the Epic Stack.
+- [Features](./features.md) - List of features the Epic Stack provides out of
+ the box.
+- [Deployment](./deployment.md) - If you skip the deployment step when starting
+ your app, these are the manual steps you can follow to get things up and
+ running.
+- [Decisions](./decisions/README.md) - The reasoning behind various decisions
+ made for the Epic Stack. A good historical record.
+- [Guiding Principles](./guiding-principles.md) - The guiding principles behind
+ the Epic Stack.
+- [Examples](./examples.md) - Examples of the Epic Stack with various tools.
+ Most new feature requests people have for the Epic Stack start as examples
+ before being integrated into the framework.
+- [Managing Updates](./managing-updates.md) - How to manage updates to the Epic
+ Stack for both the generated stack code as well as npm dependencies.
diff --git a/examples/federation/epic-stack-remote/docs/apis.md b/examples/federation/epic-stack-remote/docs/apis.md
new file mode 100644
index 0000000..415ade0
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/apis.md
@@ -0,0 +1,21 @@
+# APIs
+
+Remix routes have the ability to handle both backend code and UI code in the
+same file. Remix `loader`s and `action`s are backend code that's tightly coupled
+to the UI code for that route.
+
+Additionally, you can define routes that don't have any UI at all. These are
+called [resource routes](https://remix.run/docs/en/main/guides/resource-routes).
+This allows you to create REST endpoints or a GraphQL endpoint to make your app
+data and logic consumable by third parties or additional clients (like a mobile
+app). You can also use this to generate PDFs, images, stream multi-media and
+more.
+
+The Epic Stack has a few resource routes in place for managing images, the
+cache, and even has a few
+["full stack components"](https://www.epicweb.dev/full-stack-components) for
+components that manage the connection with their associated backend code.
+[Watch the talk](https://www.youtube.com/watch?v=30HAT5Quvgk&list=PLV5CVI1eNcJgNqzNwcs4UKrlJdhfDjshf).
+
+So, yes, you can absolutely use the Epic Stack to build APIs for consumption by
+third party clients.
diff --git a/examples/federation/epic-stack-remote/docs/authentication.md b/examples/federation/epic-stack-remote/docs/authentication.md
new file mode 100644
index 0000000..25e3425
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/authentication.md
@@ -0,0 +1,113 @@
+# Authentication
+
+The Epic Stack manages its own authentication using web standards and
+established libraries and tools.
+
+By default, the Epic Stack offers you two mechanisms for authentication:
+
+1. Username and password authentication
+2. Provider authentication
+
+## Username and password authentication
+
+When a user wishes to sign up for an account, they are asked for their email
+address. The Epic Stack will send them an email with a code as well as a link.
+The user can then enter the code or click the link to verify their email address
+which takes them through the onboarding flow which will allow them to set their
+username and password.
+
+The password is stored using the [bcrypt](https://npm.im/bcrypt) algorithm.
+
+## Provider authentication
+
+The Epic Stack ships with a system for third party authentication allowing you
+to easily add SSO (Single Sign On) to your application. The Epic Stack ships
+with support for GitHub OAuth2 authentication out of the box. But you can easily
+remove that and/or add other providers. It's all built using
+[`remix-auth`](https://npm.im/remix-auth), so any provider supported there, can
+be added, including [`web-oidc`](https://npm.im/web-oidc) which handles OpenID
+Connect authentication and exports a `remix-auth` compatible auth strategy.
+
+You can check [this example](https://github.com/kentcdodds/epic-oidc) which
+shows using OpenID Connect to add Google authentication to the Epic Stack. You
+can expand beyond this to add any other provider you'd like, and if you need to
+support SAML, you may look into
+[`@boxyhq/remix-auth-sso`](https://github.com/boxyhq/remix-auth-sso).
+
+### GitHub OAuth App
+
+You will see in `.env.example` the `GITHUB_CLIENT_ID` is set to `MOCK_...`. This
+is a precondition for a "Mock GitHub server" to be installed (with the help of
+[`msw`](https://github.com/mswjs/msw) library). See this
+[module](../tests/mocks/github.ts) for more details and pay attention to how the
+calls to `https://github.com/login/oauth/access_token` are being intercepted.
+But once deployed to an environment where `process.env.MOCKS` is not set to
+`'true'` (see how this is done when launching the
+[dev server](../server/dev-server.js) and checked in the
+[entrypoint](../index.js)), or even when developing _locally_ but not setting
+`GITHUB_CLIENT_ID` to `MOCK_...`, the requests will actually reach the GitHub
+auth server. This is where you will want to have a GitHub OAuth application
+properly set up, otherwise the logging in with GitHub will fail and a
+corresponding toast will appear on the screen.
+
+To set up a real OAuth application, log in to GitHub, go to
+`Settings -> Developer settings -> OAuth Apps`, and hit the
+`Register a new application` button. Type in `http://localhost:3000` for
+"Homepage URL" and `http://localhost:3000/auth/github/callback` for
+"Authorization callback URL". As for the `Application name` set to something
+meaningful (because your users will see the app's name), e.g.
+`MY_EPIC_APPLICATION_DEVELOPMENT`. Hit `Register application` button. You will
+be redirected to the page with your newly created OAuth app's details. You will
+see your app has got `0` users and no client secrets just yet, but the Client ID
+has already been assigned to your app. Copy over this value to
+`GITHUB_CLIENT_ID` in your _.env_ file. Now hit `Generate client secret` button,
+and copy over the newly generted secret to `GITHUB_CLIENT_SECRET` in the dotenv
+file. Hit `Update application` button on your GitHub OAuth app page.
+
+Your `.env` file should resemble this (values have been redacted):
+
+```bash
+# some other secrets and env vars
+...
+
+GITHUB_CLIENT_ID="72fa***************a"
+GITHUB_CLIENT_SECRET="b2c6d323b**************************eae016"
+```
+
+Now, run the epic-stack app in dev mode, go to login page, and use the
+`Login with GitHub` option. You will be redirected to GitHub, and prompted to
+authorize the "MY_EPIC_APPLICATION_DEVELOPMENT" (or whatever the name of your
+OAuth app is) OAuth app to access your GitHub account data. After you give your
+consent, you will be redirected to your epic-stack app running on localhost, and
+the onboarding will kick off. You can now refresh your GitHub OAuth app page and
+see how the number of registered users increased to `1`.
+
+Something to appreciate here, is that you as the GitHub OAuth app owner (since
+you created it in your GitHub account) and you as a user authorizing this GitHub
+OAuth app to access your account's data are _two different_ entities. The OAuth
+app could have been registered with an Organisation or another person's GitHub
+account.
+
+Also make sure to register separate additional OAuth apps for each of your
+deployed environments (e.g. `staging` and `production`) and specify
+corresponding homepage and redirect urls in there.
+
+## TOTP and Two-Factor Authentication
+
+Two factor authentication is built-into the Epic Stack. It's managed using a the
+[`@epic-web/totp`](https://npm.im/@epic-web/totp) (Time-based One Time
+Passwords) utility.
+
+You can read more about the decision to use TOTP in
+[the totp decision document](./decisions/014-totp.md). The secret and other
+pertinent information is stored in a `verification` model (check the Prisma
+schema). This verification model is used as the basis for all TOTP secrets. This
+is used for non-expiring Two-Factor Authentication secrets as well as temporary
+TOTP codes which are emailed to verify a user's ownership of an email/account.
+So it's used for onboarding, forgot password, and change email flows.
+
+When a user has 2FA enabled on their account, they also are required to enter
+their 2FA code within 2 hours of performing destructive actions like changing
+their email or disabling 2FA. This time is controlled by the
+`shouldRequestTwoFA` utility in the `login` full stack component in the resource
+routes.
diff --git a/examples/federation/epic-stack-remote/docs/caching.md b/examples/federation/epic-stack-remote/docs/caching.md
new file mode 100644
index 0000000..96aa227
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/caching.md
@@ -0,0 +1,84 @@
+# Caching
+
+The Epic Stack comes with caching utilities and a management dashboard that
+allows you to view and clear your cache. There are two caches built into the
+Epic Stack:
+
+- **SQLite**: This is a separate database from the main application database.
+ It's managed by LiteFS so the data is replicated across all instances of your
+ app. This can be used for long-lived cached values.
+- **LRU**: This is an in-memory cache that is used to store the results of
+ expensive queries or help deduplicate requests for data. It's not replicated
+ across instances and as it's in-memory it will be cleared when your app is
+ restarted. So this should be used for short-lived cached values.
+
+Caching is intended to be used for data that is expensive and/or slow to compute
+or retrieve. It can help you avoid costs or rate limits associated with making
+requests to third parties.
+
+It's important to note that caching should not be the first solution to slowness
+issues. If you've got a slow query, look into optimizing it with database
+indexes before caching the results.
+
+## Using the cache
+
+You won't typically interact directly with the caches. Instead, you will use
+[`cachified`](https://www.npmjs.com/package/@epic-web/cachified) which is a nice
+abstraction for cache management. We have a small abstraction on top of it which
+allows you to pass `timings` to work seamlessly with
+[the server timing utility](./server-timing.md).
+
+Let's say we're making a request to tito to get a list of events. Tito's API is
+kinda slow and our event details don't change much so we're ok speeding things
+up by caching them and utilizing the stale-while-revalidate features in
+cachified. Here's how you would use cachified to do this:
+
+```tsx
+import { cachified, cache } from '#app/utils/cache.server.ts'
+import { type Timings } from '#app/utils/timing.server.ts'
+
+const eventSchema = z.object({
+ /* the schema for events */
+})
+
+export async function getScheduledEvents({
+ timings,
+}: {
+ timings?: Timings
+} = {}) {
+ const scheduledEvents = await cachified({
+ key: 'tito:scheduled-events',
+ cache,
+ timings,
+ getFreshValue: () => {
+ // do a fetch request to the tito API and stuff here
+ return [
+ /* the events you got from tito */
+ ]
+ },
+ checkValue: eventSchema.array(),
+ // Time To Live (ttl) in milliseconds: the cached value is considered valid for 24 hours
+ ttl: 1000 * 60 * 60 * 24,
+ // Stale While Revalidate (swr) in milliseconds: if the cached value is less than 30 days
+ // expired, return it while fetching a fresh value in the background
+ staleWhileRevalidate: 1000 * 60 * 60 * 24 * 30,
+ })
+ return scheduledEvents
+}
+```
+
+With this setup, the first time you call `getScheduledEvents` it will make a
+request to the tito API and return the results. It will also cache the results
+in the `cache` (which is the SQLite cache). The next time you call
+`getScheduledEvents` it will return the cached value if the cached value is less
+than 30 days old. If the cached value is older than 24 hours, it will also make
+a request to the tito API. If the cache value is more than 30 days old, it will
+wait until the tito request is complete and then return the fresh value.
+
+Bottom line: You make the request much less often and users are never waiting
+for it. Every situation will require you think through the implications of
+caching and acceptable stale-ness, but the point is you have those levers to
+pull.
+
+A lot more needs to be said on this subject (an entire workshop full!), but this
+should be enough to get you going!
diff --git a/examples/federation/epic-stack-remote/docs/client-hints.md b/examples/federation/epic-stack-remote/docs/client-hints.md
new file mode 100644
index 0000000..2dc2641
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/client-hints.md
@@ -0,0 +1,13 @@
+# Client Hints
+
+> **NOTE:** Find background on this concept in the decision document:
+> `0005-client-pref-cookies.md`.
+
+[Watch the tip](https://www.epicweb.dev/tips/use-client-hints-to-eliminate-content-layout-shift)
+on [EpicWeb.dev](https://www.epicweb.dev):
+
+[](https://www.epicweb.dev/tips/use-client-hints-to-eliminate-content-layout-shift)
+
+This functionality has been moved into `@epic-web/client-hints` and is the
+primary source for documentation on this feature. Our integration can be found
+in `app/utils/client-hints.tsx`.
diff --git a/examples/federation/epic-stack-remote/docs/community.md b/examples/federation/epic-stack-remote/docs/community.md
new file mode 100644
index 0000000..9d0b813
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/community.md
@@ -0,0 +1,21 @@
+# Community
+
+Here you can find useful learning resources and tools built and maintained by
+the community, such as libraries, examples, articles, and videos.
+
+## Learning resources
+
+The primary learning resources for the Epic Stack is
+[EpicWeb.dev](https://www.epicweb.dev), [EpicReact.dev](https://epicreact.dev),
+and [TestingJavaScript.com](https://testingjavascript.com). On these you will
+find free and paid premium content that will help you build epic web
+applications (with or without the Epic Stack).
+
+The community has put together some additional learning resources that you may
+enjoy!
+
+### Videos
+
+- **Dark Mode Toggling using Client-preference cookies** by
+ [@rajeshdavidbabu](https://github.com/rajeshdavidbabu) - Youtube
+ [link](https://www.youtube.com/watch?v=UND-kib_iw4)
diff --git a/examples/federation/epic-stack-remote/docs/database.md b/examples/federation/epic-stack-remote/docs/database.md
new file mode 100644
index 0000000..e147301
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/database.md
@@ -0,0 +1,368 @@
+# Database
+
+## Primary Instance
+
+The way that LiteFS works is that it has a "primary instance" that is the only
+instance that can write to the database. All other instances are "replicas" that
+can only read from the database. This is a pretty common pattern for databases
+and it's how Fly's Postgres service works as well.
+
+The primary instance is determined by Fly's [consul](https://www.consul.io/)
+service and can change over time. By default, all instances are allowed to be
+primary instances. However, writes will be slower for people who are not
+geographically close to the primary instance, so the Epic Stack has configured
+consul to only allow instances in the primary region and it's recommended that
+you choose a primary region that's closest to most of your users. This
+configuration can be found in `other/litefs.yml`. The `PRIMARY_REGION` is
+determined from the `primary_region` property in the `fly.toml` file.
+
+When you initialize an Epic Stack app, it will ask you which region you wish to
+deploy to and this will set the `primary_region` in the `fly.toml` file. It is
+recommended that you deploy two instances in this region so you have zero
+downtime deploys.
+
+You can determine which instance is the primary instance by running the
+following command:
+
+```sh
+fly status --app [YOUR_APP_NAME]
+```
+
+This will show you a table of all your instances and the primary instance will
+have "ROLE" set to "primary".
+
+To deploy more regions, you can run `fly scale count`. For example, if I wanted
+to have two instances in `sjc` (my primary region) and one in `ams`, I would
+run:
+
+```sh
+fly scale count 2 --region sjc
+fly scale count 1 --region ams
+```
+
+## Connecting to your production database
+
+The location of the sqlite database is kinda funny. The real location is in
+`/data/litefs/dbs/sqlite.db`. However, during development you connect to it via
+the fake filesystem managed by LiteFS so it can propagate any changes to your
+database to all replicas.
+
+So to connect to your database, you'll want to connect to it at
+`/litefs/data/sqlite.db` in the deployed application. Because it is SQLite, you
+cannot connect to it unless you're running a command-line session on the
+machine. You can do this using `fly ssh console`. The Dockerfile simplifies this
+further by adding a `database-cli` command. You can connect to the live database
+by running `fly ssh console -C database-cli`.
+
+To connect to the deployed database from your local machine using Prisma Studio,
+you can utilize Fly's `ssh` and `proxy` commands.
+
+- Run in one terminal the command to start Prisma Studio on your desired Fly app
+ ```sh
+ fly ssh console -C "npm run prisma:studio" --app [YOUR_APP_NAME]
+ ```
+- Run in a second terminal the command to proxy your local port 5556 to Prisma
+ Studio
+ ```sh
+ fly proxy 5556:5555 --app [YOUR_APP_NAME]
+ ```
+
+If you have multiple instances of your app running, and you'd like to make edits
+to your database, you will need to run `prisma:studio` on the primary instance.
+
+- Get a list of your app instances, the `ROLE` column will show which instance
+ is `primary`
+ ```sh
+ fly status --app [YOUR_APP_NAME]
+ ```
+- Run the console command with the `-s` select flag
+ ```sh
+ fly ssh console -C "npm run prisma:studio" -s --app [YOUR_APP_NAME]
+ ```
+- Use your arrow keys to select the primary instance
+
+To work with Prisma Studio and your deployed app's database, simply open
+`http://localhost:5556` in your browser.
+
+> **Note**: You may want to add `--select` to the `fly ssh console` command to
+> select the instance you want to connect to if you have multiple instances
+> running. Otherwise you could connect to a non-primary instance. The easiest
+> way to determine the primary instance (because it can change) is to run
+> `fly status` and the row that has "ROLE" set to "primary" is the primary
+> instance. Alternatively, you can open the deployed application and check the
+> request headers. One of them will be `Fly-Primary-Instance` which will tell
+> you the instance ID of the primary instance.
+
+## Migrations
+
+Thanks to Prisma, we've got a great mechanism for handling database migrations.
+Any migrations necessary are run (by the primary instance only) as part of the
+deploy process. You can find this in the `other/litefs.yml` file.
+
+We deploy to multiple instances at once and the way we deploy means we don't
+have any downtime during deploys. However, to make this work, you do need to
+make sure you can run two versions of your app at once. Specifically, you should
+not deploy "breaking schema changes" to your app in a way that will break any
+running instances. This is a pretty typical requirement for production
+applications.
+
+The basic idea is that you maintain support for any client that is currently
+running. In Epic Stack apps that's normally just the currently running version
+of the app. So you really only need to worry about "breaking schema changes"
+whenever you deploy a new version, but you don't have to worry about avoiding
+breaking schema changes for old versions of the app that are no longer running.
+
+In practice, this means you should adopt a "widen then narrow" strategy for
+schema migrations. This is a pretty common practice, but here's a simple example
+of how this works (each step here is an individual deploy):
+
+1. Widen app to consume A or B
+2. Widen db to provide A and B and the app to write to both A and B
+3. Narrow app to consume B and only write to B
+4. Narrow db to provide B
+
+So, let's say that today your app allows users to provide a "name" and you want
+to change that to `firstName` and `lastName` instead. Here's how you'd do that
+(again, each of these steps end in a deploy):
+
+1. Widen app to consume `firstName` and `lastName` or `name`. So all new code
+ that references the `firstName` and `lastName` fields should fallback to the
+ `name` field and not error if the `firstName` and `lastName` fields don't
+ exist yet, which it won't at this point.
+2. Widen db to provide `firstName` and `lastName` and `name`. So the `name`
+ field should be populated with the `firstName` and `lastName` fields. You can
+ do this as part of the migration SQL script that you run. The easiest way to
+ do this is to generate the migration script to add the fields using
+ `prisma migrate` and then modify the script to copy the existing data in the
+ `name` field to the `firstName` field (maybe with the help of VSCode Copilot
+ π
).
+3. Narrow app to consume `firstName` and `lastName` by only writing to those
+ fields and removing the fallback to the `name` field.
+4. Narrow db to provide `firstName` and `lastName` by removing the `name` field.
+ So now you can remove the `name` field from the db schema.
+
+By following this strategy, you can ensure zero downtime deploys and schema
+migrations.
+
+## Seeding Production
+
+In this application we have Role-based Access Control implemented. We initialize
+the database with `admin` and `user` roles with appropriate permissions.
+
+This is done in the `migration.sql` file that's included in the template. If you
+need to seed the production database, modifying migration files manually is the
+recommended approach to ensure it's reproducible.
+
+The trick is not all of us are really excited about writing raw SQL (especially
+if what you need to seed is a lot of data), so here's an easy way to help out:
+
+1. Create a script very similar to our `prisma/seed.ts` file which creates all
+ the data you want to seed.
+ ```sh nonumber
+ cp prisma/seed.ts ./prod-seed.local.ts
+ ```
+ Then modify that file to create the data you want to seed.
+1. Create a temporary database file to seed the data into.
+ ```sh
+ DATABASE_URL=file:./seed.local.db npx prisma migrate reset --skip-seed --force
+ ```
+1. Run the custom seed script locally to generate the data you want to seed.
+ ```sh
+ DATABASE_URL=file:./seed.local.db npx tsx ./prod-seed.local.ts
+ ```
+1. Create a "dump" of the seed database using the `sqlite3` command line tool.
+ ```sh nonumber
+ sqlite3 ./prisma/seed.local.db .dump > seed.local.sql
+ ```
+1. Copy the relevant bits from the `seed.local.sql` file into your
+ `migration.sql` file. The `seed.local.sql` will include create table/index
+ lines etc. which should already be in your `migration.sql`. You probably just
+ want `INSERT` commands.
+1. Deploy your app and verify that the data was seeded correctly.
+
+If your app has already applied all migrations, then the changes to the
+`migration.sql` won't be applied (because prisma's already applied it). So then
+you can run the following command to apply the migration:
+
+```sh nonumber
+fly ssh console -C "npx prisma migrate reset --skip-seed --force" --app [YOUR_APP_NAME]
+```
+
+> **WARNING**: This will reset your database and apply all migrations. Continue
+> reading if you want to avoid this.
+
+If you have existing data in your production database and you'd like to seed it
+with more data without performing a migration, then it's a bit more involved.
+
+1. Backup your production database.
+1. Create a new database file (locally) with the data you want to seed.
+1. Create a "dump" of the seed database using the `sqlite3` command line tool.
+ ```sh nonumber
+ sqlite3 seed.db .dump > seed.sql
+ ```
+1. Copy the `seed.sql` file to your production volume next to your database (via
+ `fly sftp`)
+1. SSH into your production server and run the following command:
+ ```sh nonumber
+ sqlite3 /tmp/data.db < seed.sql
+ ```
+1. Import the data into your database via litefs:
+ ```sh nonumber
+ litefs import -name sqlite.db /tmp/data.db
+ ```
+1. Verify that your production database has been seeded correctly. If it hasn't,
+ then restore your backup (asap).
+
+## Backups
+
+### Manual DB Backups
+
+Manual DB backups can be taken/restored using `litefs` commands:
+
+- `litefs export`: https://fly.io/docs/litefs/export/
+- `litefs import`: https://fly.io/docs/litefs/import/
+
+**Make sure to keep the backup in a secure location. Your DB backup will contain
+user information and password hashes!**
+
+You can manually create a backup for your database using the following steps:
+
+1. SSH into your fly instance:
+ ```sh
+ fly ssh console --app [YOUR_APP_NAME]
+ ```
+1. Create a `backups` folder:
+ ```sh
+ mkdir /backups
+ ```
+1. Create a backup file using `litefs export`, and exit the console (it is
+ recommended to name the exported file with the current date):
+ ```sh
+ litefs export -name sqlite.db /backups/backup-2023-10-10.db
+ exit
+ ```
+1. Use sftp to download the backup file:
+ ```sh
+ fly ssh sftp get /backups/backup-2023-10-10.db --app [YOUR_APP_NAME]
+ ```
+
+You can now store this backup file wherever you like, such as an S3 bucket
+(again, make sure it's a secure location!).
+
+See the fly docs for more info: https://fly.io/docs/litefs/backup/
+
+### Manual DB restoration
+
+**WARNING - THIS OVERWRITES YOUR DATABASE, YOU CAN LOSE DATA!! TAKE ANOTHER
+BACKUP OF THE CURRENT DATABASE BEFORE DOING THIS!!**
+
+1. Establish an sftp session with the fly instance and upload the backup file to
+ the server using `put`:
+ ```sh
+ fly ssh sftp shell --app [YOUR_APP_NAME]
+ put backup-2023-10-10.db
+ ```
+1. Quit the sftp session with CTRL+C
+1. SSH into the fly instance:
+ ```sh
+ fly ssh console --app [YOUR_APP_NAME]
+ ```
+1. Restore the database from the backup file using `litefs import`
+ ```sh
+ litefs import -name sqlite.db /backup-2023-10-10.db
+ ```
+1. Exit the ssh session
+ ```sh
+ exit
+ ```
+
+## Troubleshooting
+
+### Faulty Prisma Migration
+
+If you accidentally run a faulty migration on prod DB, and you see this message
+in the logs:
+
+```sh
+migrate found failed migrations in the target database, new migrations will not be applied. Read more about how to resolve migration issues in a production database: https://pris.ly/d/migrate-resolve
+```
+
+You've got a few options:
+
+1. If you don't care about the data, you can delete the app on fly and re-deploy
+ it after deleting/fixing the faulty migration.
+2. If you have a recent backup of the DB, you can restore it and re-deploy the
+ app after deleting/fixing the faulty migration. **(Make sure that the backup
+ isn't too old, otherwise you'll lose data)**
+ - You can fix a faulty migration by either editing the migration SQL file, or
+ by deleting the particular migration folder from `prisma/migrations` and
+ re-generating the migration after fixing the error.
+3. If you do care about the data and don't have a backup, you can follow these
+ steps:
+
+ 1. Comment out the
+ [`exec` section from `litefs.yml` file](https://github.com/epicweb-dev/epic-stack/blob/main/other/litefs.yml#L31-L37).
+
+ ```yml
+ # exec:
+ # - cmd: npx prisma migrate deploy
+ # if-candidate: true
+
+ # - cmd: npm start
+ ```
+
+ 2. Commit this change and deploy the app to fly.
+ - This will make sure that after building the dockerfile and deploying it
+ to the fly machine, `npx prisma migrate deploy` and `npm start` commands
+ won't be executed.
+ 3. Now that the main machine is up and running, you can SSH into it by
+ running `fly ssh console --app [YOUR_APP_NAME]` in the terminal.
+ 4. Create a backup of the DB and download it by following the steps mentioned
+ in the [Manual DB Backups](#manual-db-backups) section.
+ 5. Make a copy of the downloaded DB backup file and store it in a secure
+ location (If something were to go wrong, we have a backup of the backup).
+ 6. Write SQL or use a tool like
+ [DB Browser for SQLite](https://sqlitebrowser.org/) to remove the last
+ (failed) entry from `_prisma_migrations` table from the downloaded DB
+ backup file.
+ - If you're using DB Browser for SQLite but your DB isn't selectable when
+ selecting a file to open, change DB file extension to `sqlite` from
+ `db`. **Make sure to change it back to `db` before uploading it to the
+ fly machine**.
+ 7. Follow the steps mentioned in the
+ [Manual DB restoration](#manual-db-restoration) section to restore the
+ modified DB backup file to the fly machine.
+ 8. Now the DB is restored, but if you deploy the app with the faulty
+ migration, it will fail again. So you need to delete/fix the faulty
+ migration.
+ - You can fix a faulty migration by either editing the migration SQL file,
+ or by deleting the particular migration folder from `prisma/migrations`
+ and re-generating the migration after fixing the error.
+ 9. Uncomment the
+ [`exec` section from `litefs.yml` file](https://github.com/epicweb-dev/epic-stack/blob/main/other/litefs.yml#L31-L37).
+
+ ```yml
+ exec:
+ - cmd: npx prisma migrate deploy
+ if-candidate: true
+
+ - cmd: npm start
+ ```
+
+ 10. Commit this change and deploy the app to fly :tada:
+
+ NOTE: Steps 5-7 can be skipped if you can write SQL to remove the faulty
+ migration from the DB. You can use the following SQL query to remove the
+ faulty migration from the DB:
+
+ ```sql
+ DELETE FROM _prisma_migrations WHERE name = 'migration_name';
+ ```
+
+ Make sure to replace `migration_name` with the name of the faulty migration.
+
+This is particularly hard because of how CONSUL leases work. You cannot just
+simply spin up another machine and access your database from it, because only
+one machine can get the primary lease. So you have to disable exec commands so
+the machine that has primary lease can start up, then you can SSH into it and
+fix the DB.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/000-template.md b/examples/federation/epic-stack-remote/docs/decisions/000-template.md
new file mode 100644
index 0000000..374aa1c
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/000-template.md
@@ -0,0 +1,12 @@
+# Title
+
+Date: YYYY-MM-DD
+
+Status: proposed | rejected | accepted | deprecated | β¦ | superseded by
+[0005](0005-example.md)
+
+## Context
+
+## Decision
+
+## Consequences
diff --git a/examples/federation/epic-stack-remote/docs/decisions/001-typescript-only.md b/examples/federation/epic-stack-remote/docs/decisions/001-typescript-only.md
new file mode 100644
index 0000000..f28ebd8
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/001-typescript-only.md
@@ -0,0 +1,44 @@
+# TypeScript Only
+
+Date: 2023-05-08
+
+Status: accepted
+
+## Context
+
+The `create-remix` CLI allows users to select whether they want to use
+JavaScript instead of TypeScript. This will auto-convert everything to
+JavaScript.
+
+There is (currently) no way to control this behavior.
+
+Teams and individuals building modern web applications have many great reasons
+to build them with TypeScript.
+
+One of the challenges with TypeScript is getting it configured properly. This is
+not an issue with a stack which starts you off on the right foot without needing
+to configure anything.
+
+Another challenge with TypeScript is handling dependencies that are not written
+in TypeScript. This is increasingly becoming less of an issue with more and more
+dependencies being written in TypeScript.
+
+## Decision
+
+We strongly advise the use of TypeScript even for simple projects and those
+worked on by single developers. So instead of working on making this project
+work with the JavaScript option of the `create-remix` CLI, we've decided to
+throw an error informing the user to try again and select the TypeScript option.
+
+We've also made the example script in the `README.md` provide a selected option
+of `--typescript` so folks shouldn't even be asked unless they leave off that
+flag in which case our error will be thrown.
+
+## Consequences
+
+This makes the initial experience not great for folks using JavaScript.
+Hopefully the Remix CLI will eventually allow us to have more control over
+whether that question is asked.
+
+This also may anger some folks who really don't like TypeScript. For those
+folks, feel free to fork the starter.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/002-email-service.md b/examples/federation/epic-stack-remote/docs/decisions/002-email-service.md
new file mode 100644
index 0000000..17bc397
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/002-email-service.md
@@ -0,0 +1,41 @@
+# Email Service
+
+Date: 2023-05-08
+
+Status: superseded by [017](017-resend-email.md)
+
+## Context
+
+When you're building a web application, you almost always need to send emails
+for various reasons. Packages like `nodemailer` make it quite easy to send your
+own emails through your own mailserver or a third party's SMTP server as well.
+
+Unfortunately,
+[deliverability will suffer if you're not using a service](https://cfenollosa.com/blog/after-self-hosting-my-email-for-twenty-three-years-i-have-thrown-in-the-towel-the-oligopoly-has-won.html).
+The TL;DR is you either dedicate your company's complete resources to "play the
+game" of email deliverability, or you use a service that does. Otherwise, your
+emails won't reliably make it through spam filters (and in some cases it can
+just get deleted altogether).
+
+[The guiding principles](https://github.com/epicweb-dev/epic-stack/blob/main/docs/guiding-principles.md)
+discourage services and encourage quick setup.
+
+## Decision
+
+We will use a service for sending email. If emails don't get delivered then it
+defeats the whole purpose of sending email.
+
+We selected [Mailgun](https://www.mailgun.com/) because it has a generous free
+tier and has proven itself in production. However, to help with quick setup, we
+will allow deploying to production without the Mailgun environment variables set
+and will instead log the email to the console so during the experimentation
+phase, developers can still read the emails that would have been sent.
+
+During local development, the Mailgun APIs are mocked and logged in the terminal
+as well as saved to the fixtures directory for tests to reference.
+
+## Consequences
+
+Developers will need to either sign up for Mailgun or update the email code to
+use another service if they prefer. Emails will actually reach their
+destination.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/003-sqlite.md b/examples/federation/epic-stack-remote/docs/decisions/003-sqlite.md
new file mode 100644
index 0000000..0a78a26
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/003-sqlite.md
@@ -0,0 +1,83 @@
+# SQLite
+
+Date: 2023-05-13
+
+Status: accepted
+
+## Context
+
+SQLite is a sql-based database with a particularly unique feature: the entire
+database is in a single file. Largely for this reason, I've historically seen it
+as a simple database for simple use cases.
+
+However, in recent years SQLite has received a great deal of development and
+attention making it a simple database for even more advanced use cases. The fact
+that SQLite is a single file on disk comes with a major benefit of 0 latency
+which
+[drastically reduces the "n+1 problem"](https://www.sqlite.org/np1queryprob.html).
+
+Another issue is database size, however,
+[SQLite is capable of handling databases that are an Exabyte in size](https://sqlite.org/hctree/doc/hctree/doc/hctree/index.html)
+(that's one million Terabytes, or one billion Gigabytes π€―).
+
+SQLite does not support subscriptions which can be a limitation on certain
+real-time use cases. However, there are plenty of reasons to recommend against
+using database subscriptions for real-time use cases anyway, and that is the
+case in the Epic Stack (as of today, we don't have real-time examples or docs,
+but when we do it's likely we wouldn't use database subscriptions anyway).
+
+SQLite being a file on disk does make connecting from external clients
+effectively impossible. This makes it more difficult to connect to it using
+database admin tools. However, it is possible to run tools like `prisma studio`
+on the machine where the sqlite volume is mounted if necessary. And the
+`Dockerfile` is already configured to allow easy SSH connections into the sqlite
+CLI. This is nowhere near as good as a proper admin tool, but there's likely
+more that could be done here to improve the experience. On my own website
+(kentcdodds.com), I am able to have prisma studio run in production protected by
+authentication and I expect we'll be able to add something like that to the Epic
+Stack in the future.
+
+SQLite does not support plugins like
+[TimescaleDB](https://github.com/timescale/timescaledb) for Postgres. While
+time-series data is possible with SQLite, I do not have experience with this use
+case and can't speak to the challenges there. My intuition says it's not
+advisable to use SQLite for that use case.
+
+SQLite does not support enums which means you're forced to use strings. I have
+mixed feelings about this, but I mostly don't like enums anyway. The main
+drawback to this is when it comes to the typings for the client which doesn't
+allow you to ensure all values of a column are only within a set of specific
+possible values for the string. However, with Prisma client extensions, handling
+this kind of enforcement at the client (and typing) level should be possible.
+This would need to be documented in the future
+[#29](https://github.com/epicweb-dev/epic-stack/issues/29).
+
+As a file on disk, you cannot "distribute" SQLite directly. However, with tools
+like [Turso](https://turso.tech/) or [LiteFS](https://fly.io/docs/litefs), you
+can make that work. And both of these tools even have built-in solutions to the
+"Read Replica Consistency" challenge. So if you need your app to run in multiple
+instances, you need to use one of these tools.
+
+Using SQLite is an _enormous_ simplification both during development and
+production. With one less service to worry about (especially something as
+critical as your database), you're less likely to experience an outage.
+Additionally, all you need is a persisted volume for your application (which you
+would need for a database service anyway), so it's less costly as well.
+
+## Decision
+
+We'll use SQLite because it satisfies the use cases of our target audience.
+
+## Consequences
+
+This means we need to have a way to connect to our SQLite database in
+production. We'll also need to have a way to easily seed the database
+(documentation will need to be written and perhaps some scripts). We want to
+support multi-region so LiteFS needs to be configured.
+
+This also means real-time use cases will need to find a solution that doesn't
+rely on database subscriptions.
+
+Ultimately, this decision drastically simplifies development, deployment,
+maintenance, and services for the web application and reduces running costs. A
+huge win for the majority of web applications.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/004-github-actions.md b/examples/federation/epic-stack-remote/docs/decisions/004-github-actions.md
new file mode 100644
index 0000000..036e398
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/004-github-actions.md
@@ -0,0 +1,49 @@
+# GitHub Actions
+
+Date: 2023-05-15
+
+Status: accepted
+
+## Context
+
+Deploying serious web applications to production on a regular basis requires
+automation. Running testing, linting, and a build before deployment is a
+accepted practice to ensure a quality product is deployed.
+
+Alongside this, we want to run this automation any time code is merged (or will
+soon be merged) from contributors to the project. This is called "Continuous
+Integration" and is necessary for teams to move confidently and focus on their
+value proposition. Make the robots do the boring stuff so we can focus on the
+creative work.
+
+We can run this automation on our own machines during development, but it can be
+easy to forget to do this. It's even harder to be confident the automation was
+run when you are trying to combine the work of multiple people (who's
+responsible to run the deploy script?). So it's best to have this automation run
+on a separate machine that's dedicated to this task.
+
+I don't want to have that machine running in my closet, so instead we need to
+look to outside services for managing this for us. There are many such services.
+Most of the target audience of the Epic Stack are familiar with GitHub and many
+use it already for other projects. The vast majority already have accounts on
+GitHub as well.
+
+GitHub has a CI service called
+[GitHub Actions](https://docs.github.com/en/actions) which satisfies all the
+necessary use cases for the Epic Stack. It does not require an additional
+account, though it does necessitate you use GitHub for hosting the code. Also,
+it is free for open source projects, but paid for private projects.
+
+## Decision
+
+We've decided to use GitHub Actions for Continuous Integration in the Epic
+Stack. We have a single action that handles running linting, type checking,
+tests, and deployment to both staging (`dev` branch) and production (`main`
+branch).
+
+## Consequences
+
+This means users of the Epic Stack must host their repositories within GitHub
+and pay for private repositories. This reduces the number of extra services
+users of the Epic Stack need to sign up for since the vast majority already have
+accounts with GitHub anyway.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/005-client-pref-cookies.md b/examples/federation/epic-stack-remote/docs/decisions/005-client-pref-cookies.md
new file mode 100644
index 0000000..719a4a0
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/005-client-pref-cookies.md
@@ -0,0 +1,94 @@
+# Client Preference Cookies
+
+Date: 2023-05-16
+
+Status: accepted
+
+## Context
+
+Server rendering is preferred for most things to avoid content layout shift
+(which causes a poor user experience). Unfortunately, there are a number of
+limitations with this because the browser doesn't give the server enough
+information about the user's preferences. For example:
+
+- `prefers-color-scheme` (light/dark mode)
+- `prefers-reduced-data`
+- time zone offset
+- locale
+
+And much more.
+
+The problem is that if what you display to the user relies on these values, then
+what the server renders could be wrong and the user will see the incorrect UI
+until client-side JavaScript can take over and correct it which causes a "Flash
+of Incorrect UI." This is a terrible user experience.
+
+This is such an issue that the web platform will (hopefully soon) be adding new
+user preferences headers to each request for us to know these values on the
+server. Read,
+[User preference media features client hints headers](https://web.dev/user-preference-media-features-headers/)
+and
+[User Locale Preferences](https://github.com/romulocintra/user-locale-client-hints).
+However, there's no telling when these features will become standard and
+implemented in all browsers Epic Stack apps target so we cannot rely on this or
+wait for it.
+
+One solution I've used on my personal website is to include a tiny bit of inline
+JavaScript in the client that runs before the user has a chance to see anything
+to correct any flaws in the UI. This is normally enough, but on extremely slow
+connections the user will still see a flash of incorrect UI. It's also quite
+complex and pretty hacky.
+
+Another solution I've seen from
+[Jacob Paris](https://www.jacobparis.com/content/remix-ssr-dates) is to simply
+use cookies to solve this problem. You simply set a cookie in the browser from
+the client for every property your application needs and then the server knows
+the user preference during the document request and server render. There are two
+limitations here:
+
+1. First time users won't have the cookie set
+2. The cookie will be stale if the user changes their preference
+
+To solve the first problem, we can simply check that the cookies are set and if
+they are not, then we instead send a minimal document that includes a tiny bit
+of JavaScript that sets the cookies and then reloads the page. This is not
+ideal, however it's effectively as harmful to the user as a redirect which many
+websites do anyway (for example, go to `https://youtube.com` and you instantly
+get redirected to `https://www.youtube.com`). Additionally, this is effectively
+how
+[the draft specification](https://wicg.github.io/user-preference-media-features-headers/#usage-example)
+for the web platform's solution works as well.
+
+To solve the second problem, we can simply keep a tiny bit of JS in the head of
+the document that does a quick check of the cookie values and if they are stale,
+then it sets them again and triggers a reload. Still not ideal, but again, it's
+better than a content layout shift. And hopefully this solution isn't permanent
+and we can remove it once the web platform offers a better solution.
+
+To take things further, we can future proof this solution a bit by trying to
+adhere to the web platform's proposed solution as closely as possible, so that
+when it does become available, we can simply switch from the cookies to headers
+and remove the JS, leaving us with few changes to make.
+
+## Decision
+
+Even though the web platform is working on a solution for this, we cannot wait
+for it. Despite page reloads being a sub-optimal user experience, it's better
+than the content layout shift (flash of incorrect UI) alternative. Therefore, we
+will use cookies and reloads to solve this problem.
+
+## Consequences
+
+The user's first page load will be a bit slower than normal (as will any page
+load after their preferences change) because we have to do a page reload to set
+the cookies. However, this is a one-time cost and the user will not experience
+this again until they change their preferences.
+
+The user will not experience content layout shift for any user preferences our
+app depends on for the server render. This is a huge win for user experience.
+
+If a user has cookies disabled, then we need to detect that and fallback to
+default values. Users in this situation will experience content layout shift,
+but there's nothing else we can do about that. Additionally, users who have
+cookies disabled will not be able to authenticate which is a separate concern
+that should be addressed in a different decision document.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/006-native-esm.md b/examples/federation/epic-stack-remote/docs/decisions/006-native-esm.md
new file mode 100644
index 0000000..fbb941b
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/006-native-esm.md
@@ -0,0 +1,38 @@
+# Native ESM
+
+Date: 2023-05-18
+
+Status: accepted
+
+## Context
+
+Oh boy, where do I start? The history of JavaScript modules is long and
+complicated. I discuss this a bit in my talk
+[More than you want to know about ES6 Modules](https://kentcdodds.com/talks/more-than-you-want-to-know-about-es-6-modules).
+Many modern packages on npm are now publishing esm-only versions of their
+packages. This is fine, but it does mean that using them from a CommonJS module
+system requires dynamic imports which is limiting.
+
+In Remix v2, ESM will be the default behavior. Everywhere you look, ESM is
+becoming more and more the standard module option. CommonJS modules aren't going
+anywhere, but it's a good idea to stay on top of the latest.
+
+Sadly, this is a bit of a "who moved my cheese" situation. Developers who are
+familiar with CommonJS modules will be annoyed by things they were used to doing
+in CJS that they can't do the same way in ESM. The biggest is dynamic (and
+synchronous) requires. Another is the way that module resolution changes. There
+are some packages which aren't quite prepared for ESM and therefore you end up
+having to import their exports directly from the files (like radix for example).
+This is hopefully a temporary problem.
+
+## Decision
+
+We're adopting ESM as the default module system for the Epic Stack.
+
+## Consequences
+
+Experienced developers will hit a couple bumps along the way as they change
+their mental model for modules. But it's time to do this.
+
+Some tools aren't very ergonomic with ESM. This will hopefully improve over
+time.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/007-sessions.md b/examples/federation/epic-stack-remote/docs/decisions/007-sessions.md
new file mode 100644
index 0000000..4e41755
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/007-sessions.md
@@ -0,0 +1,44 @@
+# Sessions
+
+Date: 2023-05-25
+
+Status: accepted
+
+## Context
+
+There are a lot of ways to manage authentication in applications on the web.
+We've chosen to use cookies to identify a user. However, you can just stick the
+user's ID in that cookie and then sign it with a secret and then you'll always
+know that the user ID in the cookie is legitimate and you won't need to go to
+the database at all to determine who is making the request (or, at least to know
+their ID).
+
+The limitation here is that once a client has a signed cookie with the user ID,
+it cannot be revoked. You can set a `maxAge` or a `expires`, but you can't
+proactively revoke it. There's also not a great way to know how many sessions
+are currently active for a given user.
+
+For many applications, being able to proactively invalidate sessions is
+necessary. My own site has this capability. Many sites will even associate some
+identifiable information about each session as well as the last time that
+session was used to display that information to the user and allow them to
+revoke specific sessions.
+
+## Decision
+
+We will add a sessions table to the built-in template's schema allowing a
+capability for a simple "revoke all" feature. Adding more information to the
+session model would be simple for anyone needing a more sophisticated session
+management strategy. This simple implementation gets people on the right foot
+with regards to session management.
+
+## Consequences
+
+This requires doing a database lookup for every authenticated request to
+determine whether there is an active session in the database for the user's
+request. This is not a problem for us since we're using SQLite and there's 0
+latency, so the query is extremely fast.
+
+The sessions table does not currently have the capability to proactively delete
+expired sessions which means it could fill up indefinitely. This would be a good
+problem for built-in cron job support to solve eventually.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/008-content-security-policy.md b/examples/federation/epic-stack-remote/docs/decisions/008-content-security-policy.md
new file mode 100644
index 0000000..f29216e
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/008-content-security-policy.md
@@ -0,0 +1,32 @@
+# Content Security Policy
+
+Date: 2023-05-27
+
+Status: accepted
+
+Update: [022-report-only-csp.md](./022-report-only-csp.md)
+
+## Context
+
+A Content Security Policy (CSP) allows a server to inform the browser about the
+sources from which it expects to load resources. This helps to prevent
+cross-site scripting (XSS) attacks by not allowing the browser to load resources
+from any other location than the ones specified in the CSP.
+
+CSPs that are overly strict can be a major pain to work with, especially when
+using third-party libraries. Still, for the most security, the CSP should be as
+strict as possible. Additional sources can be added to the CSP as needed.
+
+## Decision
+
+We configure a tight CSP for the default application using
+[helmet](https://npm.im/helmet) which is a de-facto standard express middleware
+for configuring security headers.
+
+## Consequences
+
+Applications using the Epic Stack will start with a safer default configuration
+for their CSP. It's pretty simple to add additional sources to the CSP as
+needed, but it could definitely be confusing for folks who are unaware of the
+CSP to load resources. Documentation will be needed to help people understand
+what to do when they get CSP errors.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/009-region-selection.md b/examples/federation/epic-stack-remote/docs/decisions/009-region-selection.md
new file mode 100644
index 0000000..6a04ba8
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/009-region-selection.md
@@ -0,0 +1,32 @@
+# Region Selection
+
+Date: 2023-06-02
+
+Status: accepted
+
+## Context
+
+Fly supports running your app in
+[34 regions](https://fly.io/docs/reference/regions/) all over the world. The
+Epic Stack is set up to allow you to run in as many of these regions as you
+like, but for cost reasons, it's best to start out with a single region until
+your app needs that level of scale.
+
+Region selection has an important impact on the performance of your app. When
+you're choosing a single region, you're choosing who your app is going to be
+slower for. So you really should choose the region that's closest to the most
+critical/closest users.
+
+Unfortunately, there's no way for us to know this for every app. We can't just
+select a region for you. And we also can't just select the region that's closest
+to you. We need you to actually think about and make this decision.
+
+## Decision
+
+Ask which region the app should be deployed to during setup.
+
+## Consequences
+
+Forces the developer to make a choice (goes against the "Minimize Setup
+Friction" guiding principle). However, we can make it slightly better by
+defaulting to the region that's closest to the developer.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/010-memory-swap.md b/examples/federation/epic-stack-remote/docs/decisions/010-memory-swap.md
new file mode 100644
index 0000000..a85e25f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/010-memory-swap.md
@@ -0,0 +1,41 @@
+# Memory Swap
+
+Date: 2023-06-02
+
+Status: accepted
+
+## Context
+
+Node.js based apps can use a lot of memory. And while we can scale up the memory
+on the instances that run your app, we can't scale it up infinitely. Especially
+when we want to be cost sensitive. So we need to be able to handle the case
+where your app uses more memory than is available on the instance. A solution to
+this is to use swap memory.
+
+Swap memory is a way to use disk space as memory. It's not as fast as real
+memory, but it's better than crashing. And it's a lot cheaper than scaling up
+the memory on your instances. It makes sense for many types of apps (even at
+scale) to use swap memory. Especially for apps just getting off the ground,
+making use of swap memory can be a great way to keep costs down.
+
+Because our app is running in a container with a mounted volume, we can't use
+the normal swap memory mechanisms. Instead, we need to use a swap file. This
+means we need to create a file on the mounted volume and then use that file as
+swap memory using `fallocate`, `mkswap`, and `swapon`.
+
+Size of the swap file is pretty subjective to the application and situation. The
+Epic Stack app memory starts at 256MB on Fly. Based on that amount of memory, a
+good rule of thumb for the size of the swap file is 2-4x the size of memory,
+which would put the swap file at 512MB-1GB (for a 2GB+ RAM system, you typically
+want the swap file to be the same size as the memory). Considering our volumes
+are set to 1GB for starters, we'll start with a 512MB swap file.
+
+## Decision
+
+During app startup, we'll create a swap file on the mounted volume and then use
+that file as swap memory for the application.
+
+## Consequences
+
+In high utilization situations, we will have degraded performance instead of a
+crash. This is a good tradeoff for most apps.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/011-sitemaps.md b/examples/federation/epic-stack-remote/docs/decisions/011-sitemaps.md
new file mode 100644
index 0000000..4bc6d54
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/011-sitemaps.md
@@ -0,0 +1,33 @@
+# Sitemaps
+
+Date: 2023-06-05
+
+Status: deprecated
+
+> Update: The contribution in
+> [#456](https://github.com/epicweb-dev/epic-stack/pull/456) made it quite easy
+> to handle a sitemap so this decision has been reversed.
+
+## Context
+
+[Sitemaps](https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview)
+are useful to help website crawlers (like search engines) find all the content
+on your website. Most of the time they aren't necessary if you're linking
+between pages well. However, for large websites with lots of content that are
+highly search engine sensitive, they can be useful.
+
+It's normally not a big deal to get them wrong if you don't care about it, but
+if you really don't care about it, having the code for it can get in the way and
+it's kind of annoying.
+
+## Decision
+
+Instead of building a sitemap into the template, we'll use
+[an example](/docs/examples.md) people can reference to add a sitemap to their
+Epic Stack sites if they like.
+
+## Consequences
+
+This turns sitemaps into an opt-in for developers using the Epic Stack. Most
+people using the Epic Stack probably don't need a sitemap, and those who do will
+only need a few minutes of following the example to get it working.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/012-cuid.md b/examples/federation/epic-stack-remote/docs/decisions/012-cuid.md
new file mode 100644
index 0000000..bd35860
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/012-cuid.md
@@ -0,0 +1,81 @@
+# CUID
+
+Date: 2023-06-06
+
+Status: accepted
+
+## Context
+
+I noticed there were a couple models that were using `uuid`. To verify my
+understanding of the `uuid` vs `cuid` debate, I took it to ChatGPT.
+
+
+ChatGPT conversation: CUID vs UUID Debate
+
+**CUID vs UUID Debate** -
+[June 6, 2023](https://chat.openai.com/share/cc0eb433-9bc0-48ea-861a-dfc7ed5acf43)
+
+**Kent:** Please argue both sides of the cuid vs uuid debate.
+
+**ChatGPT:** Sure! Let's explore both sides of the debate between CUID
+(Collision-resistant Unique ID) and UUID (Universally Unique Identifier).
+
+Arguments in favor of CUID:
+
+1. Simplicity and readability: CUIDs are designed to be simple and
+ human-readable, consisting of a combination of lowercase letters and
+ numbers. This makes them easier to work with and understand compared to the
+ alphanumeric strings of UUIDs.
+2. Shorter length: CUIDs are typically shorter in length compared to UUIDs,
+ which can be advantageous in certain scenarios where storage or transmission
+ efficiency is a concern. The shorter length can lead to reduced memory usage
+ and faster processing times.
+3. Collision resistance: CUIDs are specifically designed to minimize the
+ chances of collision, meaning the probability of generating duplicate IDs is
+ extremely low. This makes them suitable for scenarios where uniqueness is
+ crucial, such as database keys or distributed systems.
+
+Arguments in favor of UUID:
+
+1. Universality and standardization: UUIDs are widely recognized and used
+ across different systems and programming languages. They have a standardized
+ format and are supported by various libraries and frameworks, making them
+ easier to integrate into existing systems and ensure compatibility.
+2. Higher entropy and randomness: UUIDs typically have a higher degree of
+ randomness due to the inclusion of a timestamp and a unique identifier from
+ the host or network. This can be beneficial in situations where security and
+ unpredictability are essential, such as generating secure session tokens or
+ cryptographic keys.
+3. Widely adopted solutions: UUIDs have been widely adopted and proven to be
+ effective in various use cases, including distributed systems, databases,
+ and web applications. This extensive usage has resulted in a wealth of
+ knowledge, libraries, and best practices associated with working with UUIDs,
+ making them a reliable choice.
+
+In summary, the choice between CUID and UUID depends on the specific
+requirements of the application or system. CUIDs offer simplicity, shorter
+length, and collision resistance, making them suitable for scenarios where
+readability and uniqueness are critical. On the other hand, UUIDs provide
+universality, standardization, higher entropy, and wider adoption, making them a
+strong choice for applications that require compatibility, security, and a
+proven track record.
+
+
+
+That pretty much checks out for my understanding of the debate. The primary
+benefit of CUIDs for the Epic Stack is their emphasis on collision resistance
+which is good for distributed systems (which is the Epic Stack). Also, it's nice
+to keep things smaller and more readable.
+
+## Decision
+
+Use CUIDs for all models instead of UUIDs.
+
+## Consequences
+
+No migration is necessary since the data type for CUIDs and UUIDs is both
+"string". If any existing application wants to proactively update old values to
+CUIDs, that's fine, but not completely necessary. All that needs to be updated
+is the prisma schema for a few of the models that were using UUIDs. Generating a
+new version of the client (which happens automatically at build time) is
+sufficient to handle this schema change.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/013-email-code.md b/examples/federation/epic-stack-remote/docs/decisions/013-email-code.md
new file mode 100644
index 0000000..c65d90f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/013-email-code.md
@@ -0,0 +1,40 @@
+# Email Verification Code
+
+Date: 2023-06-05
+
+Status: accepted
+
+## Context
+
+When a new user registers, we need to collect their email address so we can send
+them a password reset link if they forget their password. Applications may also
+need the email for other reasons, but whatever the case may be, we need their
+email address, and to reduce spam and user error, we want to verify the email as
+well.
+
+Currently, the Epic Stack will send the email with a link which the user can
+then click and start the onboarding process. This works fine, but it often means
+the user is left with a previous dead-end tab open which is kind of annoying
+(especially if they are on mobile and the email client opens the link in a
+different browser).
+
+An alternative to this is to include a verification code in the email and have
+the user enter that code into the application. This is a little more work for
+the user, but it's not too bad and it means that the user can continue their
+work from the same tab they started in.
+
+This also has implications if people want to add email verification for
+sensitive operations like password resets. If a code system is in place, it
+becomes much easier to add that verification to the password reset process as
+well.
+
+## Decision
+
+We will support both options. The email will include a code and a link, giving
+the user the option between the two so they can select the one that works best
+for them in the situation.
+
+## Consequences
+
+This requires a bit more work, but will ultimately be a better UX and will pave
+the way for other features in the future.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/014-totp.md b/examples/federation/epic-stack-remote/docs/decisions/014-totp.md
new file mode 100644
index 0000000..0d11027
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/014-totp.md
@@ -0,0 +1,114 @@
+# TOTP (Time-based One-time Password Algorithm)
+
+Date: 2023-06-06
+
+Status: accepted
+
+## Context
+
+As explained in [013-email-code.md](./013-email-code.md), user registration
+requires email verification. To verify the email address, we send the user an
+email with a magic link that includes an encrypted payload. We do something
+similar for the "forgot password" flow as well.
+
+Early on, it was decided to not bother creating a record with the database for
+new users. This simplifies things a bit, but it also means that the salt is the
+same for all users which technically opens us up for
+[rainbow table](https://en.wikipedia.org/wiki/Rainbow_table) attacks. We could
+also have implemented this using JWTs because the payload doesn't actually
+contain any sensitive information and all we need is to verify that someone else
+didn't generate the token (which is what JWTs enable).
+
+The real issue is that since the payload is known (our source for this is
+literally open) and the salt is known (it's the same for all users), a dedicated
+adversary with enough time and resources could brute-force their way to
+determine the `ENCRYPTION_SECRET`... Or a disgruntled employee could leak it.
+
+If the `ENCRYPTION_SECRET` were to somehow determined by an adversary, it would
+allow them to generate magic links for any email address and login as any user.
+Even if the risk is a little low, it's still a risk that would be nice to avoid
+provided it didn't impose an enormous effort.
+
+One way I explored for reducing this risk is rotating the encryption secret by
+having a special database table for storing the secret and then having a
+background job that rotates the secret every so often. This would also mean that
+we need to keep old secrets around for as long as the encrypted data is valid so
+we can decrypt it. We don't yet have background job support, but we're planning
+on adding it eventually (we probably should do something like this for our
+`INTERNAL_COMMAND_TOKEN` in the future). In any case, it would be nice to avoid
+the extra complexity.
+
+And so we come to TOTP (Time-based One-Time Passwords). TOTP is a standard for
+generating one-time passwords that are only valid for a specific amount of time.
+The most common example of this is the 2FA codes that you get when logging into
+a website.
+[Read how TOTP works on Wikipedia](https://en.wikipedia.org/wiki/Time-based_one-time_password).
+In our case it's pretty simple because we are both the client and server.
+
+Here's the TL;DR:
+
+1. Generate a secret key
+2. Generate the TOTP with that key
+3. Store the key and the TOTP in the database along with the thing you're
+ verifying (like the user's email address)
+4. Send the TOTP to the user (email it to them)
+5. When the user enters the TOTP, verify it against the one in the database
+ that's associated with the thing they're verifying
+6. If the TOTP is valid, delete it from the database and allow the user to
+ proceed
+
+So yeah, it's literally a short-lived, one-time password.
+
+Think of it this way: Before, we generated a one-time password (the token of the
+magic link) using the same secret. Now we generate a one-time password using a
+random secret. It becomes impossible for someone to brute-force discovery of our
+secret.
+
+**Why not just send the secret key instead of generating a TOTP?** First of all,
+we want to make sure this is something easy for a user to type in. A 6-digit
+number is much easier to type than a 32-character string. So we could make the
+secret key be 6 digits long, but the other benefit of the TOTP is that it
+expires after a certain amount of time by design. So brute force attacks are
+much less likely to succeed.
+
+On top of all that, putting this together makes features like 2FA much easier to
+implement because we can just reuse the same code.
+
+One unfortunate aspect of using a TOTP instead of an environment variable is you
+now need to store the secret key in the database. Personally I really liked that
+the email verification was effectively stateless, but I think the security
+benefits of using a TOTP outweigh the costs.
+
+As for generating the TOTP, there are a few libraries for this, but every one
+that I could find either did way too much or hasn't been updated in many years
+and has a number of flaws and limitations. So we'll need to implement our own
+based on the simplest implementation I could find: [`notp`](https://npm.im/notp)
+(it doesn't support custom algorithms and uses `sha1` π¬). A perfectly good TOTP
+can be generated in less than a hundred lines of Node.js code.
+
+A lot of this is inspired by
+[this conversation with ChatGPT 4](https://chat.openai.com/share/a1bbd00d-c9d7-4846-a9af-12c6a475cd20).
+It's a really good conversation and I recommend reading it.
+
+## Decision
+
+We'll change the magic link token to be an TOTP instead of an encrypted payload.
+
+## Consequences
+
+This makes it much easier to implement the email code verification feature
+decided on in [013-email-code.md](./013-email-code.md). It also makes it easier
+to implement 2FA which we'll do in the future. This also allows us to remove the
+`ENCRIPTION_SECRET` from the list of environment variables you need to manage.
+And we can swap the `encryption.server.ts` utility for a `totp.server.ts`
+utility which, while not simpler is a fair trade (and could definitely be
+developed and externalized into an open source library). **Update:** This has
+now been published as [@epic-web/totp](https://npm.im/@epic-web/totp).
+
+This also means we now need a new table in the database. This can be designed in
+such a way that there's no migration cost and it's purely additive.
+
+Eventually, we'll want to set up a background job that deletes expired TOTPs
+from the database. It's not a ship stopper for this feature, but something we'll
+want to have implemented eventually (the same applies to expired sessions as
+well).
diff --git a/examples/federation/epic-stack-remote/docs/decisions/015-monitoring.md b/examples/federation/epic-stack-remote/docs/decisions/015-monitoring.md
new file mode 100644
index 0000000..545f2a5
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/015-monitoring.md
@@ -0,0 +1,36 @@
+# Monitoring
+
+Date: 2023-06-09
+
+Status: accepted
+
+## Context
+
+Unless you want to be watching your metrics and logs 24/7 you probably want to
+be notified when users experience errors in your application. There are great
+tools for monitoring your application. I've used Sentry for years and it's
+great.
+
+One of the guiding principles of the project is to avoid services. The nature of
+application monitoring requires that the monitor not be part of the application.
+So, we necessarily need to use a service for monitoring.
+
+One nice thing about Sentry is it is open source so we can run it ourselves if
+we like. However, that may be more work than we want to take on at first.
+
+## Decision
+
+We'll set up the Epic Stack to use Sentry and document how you could get it
+running yourself if you prefer to self-host it.
+
+We'll also ensure that we defer the setup requirement to later so you can still
+get started with the Epic Stack without monitoring in place which is very useful
+for experiments and makes it easier to remove or adapt to a different solution
+if you so desire.
+
+## Consequences
+
+We tie the Epic Stack to Sentry a bit, but I think that's a solid trade-off for
+the benefit of production error monitoring that Sentry provides. People who need
+the scale where Sentry starts to cost money (https://sentry.io/pricing/) will
+probably be making money at that point and will be grateful for the monitoring.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/016-source-maps.md b/examples/federation/epic-stack-remote/docs/decisions/016-source-maps.md
new file mode 100644
index 0000000..d7c2de3
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/016-source-maps.md
@@ -0,0 +1,70 @@
+# Source Maps
+
+Date: 2023-06-14
+
+Status: superseded by [034-source-maps](034-source-maps.md)
+
+## Context
+
+If you're unfamiliar with source maps, check out
+[What are source maps?](https://web.dev/source-maps/) on web.dev.
+
+For anyone familiar with source maps, it's pretty obvious that you do want these
+in production for debugging purposes (read
+[Should I Use Source Maps in Production? ](https://css-tricks.com/should-i-use-source-maps-in-production/)).
+However, when you enable source maps with Remix, you get a warning that looks
+like this:
+
+```
+> remix build --sourcemap
+
+Building Remix app in production mode...
+
+β οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈ
+You have enabled source maps in production. This will make your server-side code visible to the public and is highly discouraged! If you insist, please ensure you are using environment variables for secrets and not hard-coding them into your source!
+β οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈβ οΈ
+```
+
+It's pretty well understood that source maps allow your code to be visible to
+the public and yet it's still pretty well understood that's fine because
+client-side apps send the code anyway and that can typically be reverse
+engineered (especially with the help of modern AI tools as well).
+
+The reason it's a special concern for some frameworks like Remix today is
+because the source maps for the client-side code include some of the server-side
+code as well. This is because Remix and other frameworks like it have the
+ability for you to write your server code in the same file as your browser code.
+
+The ability for the public to view your server-side code understandably raises
+some concerns for folks. This is especially dangerous if you have secrets
+hard-coded into your server-side code. Additionally, if you're exercising
+"security through obscurity" then you may be vulnerable to attacks if your
+server-side code is visible.
+
+On the flip side, you really shouldn't be hard-coding secrets into your
+server-side code anyway. You should be using environment variables for that.
+Additionally, if you're relying on "security through obscurity" then you're
+probably not as secure as you think you are.
+
+Also, source maps are necessary for error monitoring with tools like Sentry.
+Without source maps, you'll only see the minified code in your error monitoring
+tools. This makes it much harder to debug errors in production. And if you're
+debugging your application in production you'll also be limited to minified code
+as well.
+
+It may be possible to generate the source maps and make them available to
+Sentry, but then prevent them from being sent to the client. More research is
+needed to determine whether this is possible.
+
+## Decision
+
+We've decided to enable source maps in production by default. This will allow
+for better error monitoring and debugging in production. It will also allow for
+easier debugging of server-side code in production.
+
+## Consequences
+
+Developers using the Epic Stack will see an warning message during the build and
+if they don't practice good secret "hygiene" they may be vulnerable to attacks.
+So we'll add documentation explaining how to properly use environment variables
+for secrets and not hard-code them into your source code.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/017-resend-email.md b/examples/federation/epic-stack-remote/docs/decisions/017-resend-email.md
new file mode 100644
index 0000000..a52f7c8
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/017-resend-email.md
@@ -0,0 +1,29 @@
+# Migrating to Resend
+
+Date: 2023-06-20
+
+Status: accepted
+
+## Context
+
+Mailgun changed their pricing model to make it more difficult to understand what
+is available within the free tier which motivated us to re-evaluate our
+selection here. While mailgun is still a fine service,
+[Resend](https://resend.com/) has caught the attention of several users of the
+Epic Stack. It has a generous (and obvious) free tier of 3k emails a month. They
+check all the boxes regarding table-stakes features you'd expect from an email
+service. On top of those things, the UI is simple and easy to use. It's also a
+lot cheaper than Mailgun.
+
+## Decision
+
+We'll migrate to Resend. As a part of this migration, we're going to avoid
+coupling ourselves too closely to it to make it easier to switch to another
+provider if you so desire. So we'll be using the REST API instead of the SDK.
+
+## Consequences
+
+Code changes are relatively minimal. Only the `app/utils/email.server.ts` util
+and the mock for it need to be changed. Then we also need to update
+documentation to use the Resend API key instead of the mailgun sending domain,
+etc.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/018-images.md b/examples/federation/epic-stack-remote/docs/decisions/018-images.md
new file mode 100644
index 0000000..c94b743
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/018-images.md
@@ -0,0 +1,61 @@
+# Images
+
+Date: 2023-06-23
+
+Status: accepted (for now)
+
+## Context
+
+One of the guiding principles of the Epic Stack is to limit services (including
+the self-managed variety). Depending on the needs of your application, you may
+be justified in reaching for a service to solve this problem. If you have many
+large images, then finding a service that can host them for you makes a lot of
+sense.
+
+Currently, the Epic Stack stores images in the SQLite database as a blob of
+bytes. At first glance, you may think this is a really bad idea, and for some
+use cases it definitely would be. But it scales surprisingly well (in some
+cases,
+[serving small files from SQLite can be faster than the file system](https://www.sqlite.org/fasterthanfs.html)).
+In fact, thanks to LiteFS, you get the benefits of replicated storage to all
+your app nodes.
+
+Currently, the setup is pretty sub-optimal. There's currently no optimization or
+compression of these images. Whatever goes in is what comes out regardless of
+needs of the client requesting the image. And if you plan on handling a lot of
+images, you could bump against the limits of SQLite + LiteFS (it's been tested
+up to 10GBs).
+
+These limits should be fine for a large number of applications, but we don't
+want "fine" we want Epic!
+
+Another guiding principle of the Epic Stack is to make things adaptable. We
+haven't really come around to this for images yet, but hopefully in the future
+there will be a good solution to making it easy to swap from the self-hosted
+images to a service.
+
+We also have plans to support automatic optimization of images a la
+Cloudinary/Cloudflare.
+
+One thing we're waiting on is
+[LiteFS to support object storage](https://github.com/superfly/litefs/issues/327).
+Once that's done, then we'll probably move the images to files in your volume
+and we'll also be able to use that to cache optimized versions of the images.
+This will have limited scale, but should be Epic for many applications.
+
+But all of this is work that hasn't been done yet, so if you're adopting the
+Epic Stack, you may consider adjusting the image to use a service. And if you've
+got big plans for images in your site, you may want to consider a service.
+
+## Decision
+
+We'll leave things as they are for now mostly due to time constraints. Examples
+of using the Epic Stack with services are encouraged and welcome. We definitely
+want to make it easy to swap out the self-hosted images for a service, so help
+there would be appreciated as well.
+
+## Consequences
+
+People may start off projects that have ambitious image needs without realizing
+the image setup here will not satisfy their requirements. A migration would be
+annoying, but definitely possible.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/019-components.md b/examples/federation/epic-stack-remote/docs/decisions/019-components.md
new file mode 100644
index 0000000..fb0bbfd
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/019-components.md
@@ -0,0 +1,80 @@
+# Components
+
+Date: 2023-06-27
+
+Status: accepted
+
+## Context
+
+The web platform is severely lacking in terms of UI components. There's pretty
+minimal by way of built-in components, and for many that do exist, they are
+extremely difficult (if not impossible) to style.
+
+I have yet to build a web application where the product owner was happy with the
+user agent styles and capabilities of components that are built into the web
+platform.
+
+Unfortunately, building components that work nicely with assistive technologies
+in any way you would consider "accessible" is non-trivial. It's certainly not
+something that you'd want to do in every new project.
+
+So using a library that provides accessible components users expect is
+definitely a good idea. However, many component libraries are difficult to
+customize from a styling perspective.
+
+What's best is to get a "headless" UI library: One which handles the logic of
+accessible, reusable components, but leaves the styling up to you.
+
+While it may make sense to just say "let's use web components" I'm going to
+leave the argument against web components to
+[Rich Harris](https://dev.to/richharris/why-i-don-t-use-web-components-2cia)
+(he's right, and it pains me just like it does him). The Epic Stack comes with
+React, so a component library that leans on React is no problem and actually a
+nice benefit.
+
+Having been around the block a few times myself (and even having built a handful
+of component libraries), I've determined the library that does this best is
+[Radix](https://www.radix-ui.com/). It's a terrific collection of primitive
+components that has a fantastically composable API.
+
+The Epic Stack started with Radix from the start for this reason.
+
+That leaves us with the decision about how to style things. The Epic Stack
+started with Tailwind for styling (no decision document has been written about
+this choice yet), and shipped with things styled by Tailwind. It has worked
+relatively well, but the structure has been challenging for folks adopting the
+Epic Stack. It's left adopters of the Epic Stack with a lot challenges around
+customization.
+
+Customization is always the biggest challenge when it comes to styling
+components. Every company wants its own take on the UI, so having a component
+library that comes with its styles baked in is a non-starter. This is why we
+chose a headless component library in the first place.
+
+This is where [shadcn/ui](https://ui.shadcn.com/) comes into the picture. It's
+not a component library, but more of a code registry where you can
+copy/paste/modify the code to your heart's content. Additionally, it comes
+opinionated with our own opinions! It's built with Tailwind and Radix.
+
+Additionally, while you can easily copy/paste/modify from the website, you can
+also use the CLI to download components as needed. So we can add a configuration
+file to the Epic Stack and the CLI will know exactly where to place files.
+
+On top of that, shadcn/ui assumes a Tailwind setup that relies heavily on CSS
+variables for color styles which makes it much easier to adapt to the light/dark
+mode theme of the Epic Stack.
+
+## Decision
+
+We'll adopt shadcn/ui, Radix, and Tailwind as the UI component solution for the
+Epic Stack. We'll move most of the custom components that are currently in the
+Epic Stack to shadcn/ui components. We'll customize those components as needed.
+
+## Consequences
+
+It's important to keep in mind that because shadcn/ui is not a component
+library, updates for these components are similar to updates in the Epic Stack
+itself: manual. There is no way to get automated updates here. And this is
+actually a good thing, even though it's a bit more work. It's a good thing
+because it means that you can customize the components as much as you want
+without worrying about breaking changes.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/020-icons.md b/examples/federation/epic-stack-remote/docs/decisions/020-icons.md
new file mode 100644
index 0000000..d32a8dc
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/020-icons.md
@@ -0,0 +1,42 @@
+# Icons
+
+Date: 2023-06-28
+
+Status: accepted
+
+## Context
+
+Icons are a critical part to every application. It helps users quickly identify
+different actions they can take and the meaning of different elements on the
+page. It's pretty well accepted that SVGs are the way to go with icons, but
+there are a few different options for how to go about doing this.
+
+Because the Epic Stack is using React, it may feel obvious to just use a
+component per icon and inline the SVG in the component. This is fine, but it's
+sub-optimal. I'm not going to spend time explaining why, because
+[this article does a great job of that](https://benadam.me/thoughts/react-svg-sprites/).
+
+SVG sprites are no less ergonomic than inline SVGs in React because in either
+case you need to do some sort of transformation of the SVG to make it useable in
+the application. If you inline SVGs, you have [SVGR](https://react-svgr.com/) to
+automate this process. So if we can automate the process of creating and
+consuming a sprite, we're in a fine place.
+
+And [rmx-cli](https://github.com/kiliman/rmx-cli) has support for automating the
+creation of an SVG sprite.
+
+One drawback to sprites is you don't typically install a library of icons and
+then use them like regular components. You do need to have a process for adding
+these to the sprite. And you wouldn't want to add every possible icon as there's
+no "tree-shaking" for sprites.
+
+## Decision
+
+Setup the project to use SVG sprites with `rmx-cli`.
+
+## Consequences
+
+We'll need to document the process of adding SVGs. It's still possible to simply
+install a library of icons and use them as components if you're ok with the
+trade-offs of that approach. But the default in the starter will be to use
+sprites.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/021-node-version.md b/examples/federation/epic-stack-remote/docs/decisions/021-node-version.md
new file mode 100644
index 0000000..8406358
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/021-node-version.md
@@ -0,0 +1,58 @@
+# Node.js LTS
+
+Date: 2023-07-03
+
+Status: accepted
+
+## Context
+
+Node.js has a regular release cycle which is documented in the
+[release schedule](https://nodejs.org/en/about/releases/). At the time of this
+writing, there are 3 stable maintained releases: 16, 18, and 20. I'll refer you
+to that documentation to understand how the release cycle works.
+
+Deciding which version of Node.js to use for a project is a trade-off between
+using the latest features and stability.
+
+The Epic Stack is more focused on stably shipping web apps than experimenting
+with the latest features which is where the Active Long-Term Support (LTS)
+version really shines.
+
+We deploy our apps in Docker containers, and there are various base images we
+can use as options which you can find on
+[the Node.js Docker Hub](https://hub.docker.com/_/node). Aside from the version,
+there flavors of the base image which are based on the Linux distribution used.
+Feel free to read more about the different flavors on Docker Hub. One of the
+goals for us here is to not ship more than we need in production.
+
+An additional consideration we'll add as context here is what version of Linux
+to have our base image built on. With the same pragmatic approach as the Node.js
+version we want to balance latest features with stability. We'll use the
+[Debian release cycle](https://wiki.debian.org/DebianReleases) as a guide for
+this.
+
+## Decision
+
+Use the current LTS version of Node.js as the default in the starter.
+
+We'll use the `slim` flavor of the node.js images.
+
+We'll use the `bookworm` flavor of the node.js images (which is the current
+stable version of Debian: v12).
+
+## Consequences
+
+Folks should hopefully run into few compatibility issues. It's possible they
+will need features that are not back-ported to the current active LTS version,
+however it's trivial to update the Node.js version. Added documentation to the
+[managing updates](#app/managing-updates.md) docs should help people manage
+this.
+
+We'll need to update the Node.js version in the starter whenever the active LTS
+version changes.
+
+Folks who need a bunch more out of their operating system packages will need to
+switch from the `slim` flavor which only involves updating the `Dockerfile`. It
+is possible some will not realize they need more than `slim` until they run the
+Docker image (which many people will only do in production). However the
+likelihood of this impacting anyone is pretty low.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/022-report-only-csp.md b/examples/federation/epic-stack-remote/docs/decisions/022-report-only-csp.md
new file mode 100644
index 0000000..4a7b7c5
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/022-report-only-csp.md
@@ -0,0 +1,31 @@
+# Report-only CSP
+
+Date: 2023-07-14
+
+Status: accepted
+
+## Context
+
+The Epic Stack uses a strict
+[Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP).
+All the reasons for this explained in
+[the decision document](./008-content-security-policy.md) still apply. However,
+As people adapt the Epic Stack to their own needs, they may easily forget to add
+important sources to the CSP. This can lead to a frustrating experience for new
+users of the Epic Stack.
+
+There's an option for CSPs called `report-only` which allows the browser to
+report CSP violations without actually blocking the resource. This turns the CSP
+into an opt-in which follows our [guiding principle](#app/guiding-principles.md)
+of "Minimize Setup Friction" (similar to deferring setup of third-party services
+until they're actually needed).
+
+## Decision
+
+Enable report-only on the CSP by default.
+
+## Consequences
+
+New users of the Epic Stack won't be blocked by the CSP by default. But this
+also means they won't be as safe by default. We'll need to make sure enforcing
+the CSP is documented well.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/023-route-based-dialogs.md b/examples/federation/epic-stack-remote/docs/decisions/023-route-based-dialogs.md
new file mode 100644
index 0000000..4fd92eb
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/023-route-based-dialogs.md
@@ -0,0 +1,45 @@
+# Route-based Dialogs (aka Modals)
+
+Date: 2023-07-14
+
+Status: accepted
+
+## Context
+
+Dialogs (also known as modals) are often a crutch for poor UX design. They are
+often used when you haven't thought through the design of the page within the
+context of the user's intentions.
+
+They aren't always bad though. Sometimes they are useful to provide a
+confirmation step before a destructive action. For this we already have the
+`useDoubleCheck` hook which makes it easier to help the user confirm their
+action, but using a dialog gives you the opportunity to explain to the user a
+bit more before the action is completed.
+
+However, using Dialogs for routes is problematic. Dialogs without animations are
+poor UX. But server rendering animations is problematic because it means the
+user has to wait for the animation code to load before they see the content they
+came for.
+
+Unsplash solves this problem by using dialogs for images when you click on them,
+but when you refresh the page you see that image's page. This is an intentional
+decision by them and I'm sure they weighed the pros and cons for this UX.
+However, it's not often this is a good user experience.
+
+Until today, the Epic Stack used route-based dialogs for the 2FA flow and the
+avatar edit experience. I like using routes for these so it's easy to link the
+user directly to these pages and makes it easier to navigate in and out of them.
+
+These are definitely not a good use of route-based dialogs. It certainly doesn't
+make sense to render it as a dialog for a client-navigation but something else
+for landing on that page like unsplash does for its images.
+
+## Decision
+
+Remove route-based dialogs from the Epic Stack.
+
+## Consequences
+
+A better UX. What used to be dialogs will now simply be pages. To help with
+navigation, we'll need to use breadcrumbs to help the user orient themselves and
+find a way back to where they came from.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/024-change-email.md b/examples/federation/epic-stack-remote/docs/decisions/024-change-email.md
new file mode 100644
index 0000000..e084ed9
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/024-change-email.md
@@ -0,0 +1,68 @@
+# Change Email
+
+Date: 2023-07-26
+
+Status: accepted
+
+## Context
+
+For most websites, the user's email is the primary mechanism for authenticity.
+You sign up with your email, password reset involves the email, and often you
+sign in with your email.
+
+For various reasons, users may wish to maintain their account data, but change
+their email address. There are a few important considerations for managing this:
+
+1. The user may no longer have access to the current email address.
+1. If the user typos their email address, then they may lose access to their
+ account.
+1. If an adversary gets temporary access to a user's account, they may be able
+ to change the victim's email address to one they own.
+
+There are a few ways to address these concerns. Here are a few things you could
+do (some in combination)
+
+- Let the user change the email with no validation
+- Notify the new email address of the change
+- Notify the old email address of the change
+- Require confirmation of the new address before allowing the change
+- Require confirmation of the old address before allowing the change
+- Require a two-factor code before allowing the change
+
+The ultimate secure approach would be:
+
+- Require a two-factor code and confirmation from the old and new address before
+ allowing the change.
+
+This has a few problems:
+
+1. Not all users have 2FA enabled
+2. Users don't always have access to their old address
+
+If you really needed that level of security, you could require 2FA and users
+could reach out to support if they don't have access to the old email to plead
+their case.
+
+However, there's a middle-ground:
+
+- Require a two-factor code from users who have it enabled, receive confirmation
+ of the new address, and notify the old address.
+
+This strikes a good balance of being easy for the user, reducing the number of
+support requests, and security.
+
+## Decision
+
+We're going to require recent (within the last 2 hours) verification of the
+two-factor code if the user has it enabled, require confirmation of the new
+address, and notify the old address of the change.
+
+## Consequences
+
+This will require supporting a mechanism for tracking when the last 2FA code was
+entered (just storing the time in the session). This will also require a new
+verification for confirming the new address (utilizing existing verification
+utilities we have for onboarding the user in the first place).
+
+It's a little bit more complicated than just letting the user change their email
+address, but will hopefully reduce the problems described.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/025-rate-limiting.md b/examples/federation/epic-stack-remote/docs/decisions/025-rate-limiting.md
new file mode 100644
index 0000000..4419cbf
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/025-rate-limiting.md
@@ -0,0 +1,62 @@
+# Rate Limiting
+
+Date: 2023-08-10
+
+Status: accepted
+
+## Context
+
+Adversaries can sometimes attempt to break into user's accounts by guessing
+their passwords. This is known as a brute force attack.
+
+Or, sometimes a bad guy may not like you and want to do you harm so they will
+trigger you to send people a lot of emails by hitting your `/signup` or
+`/settings/profile/change-email` endpoint over and over again. Doing this will
+reduce your reputation with email providers and may cause your emails to be
+flagged as spam.
+
+A common way to reduce the impact and likelihood of this is to rate limit
+requests. This means that you only allow a certain number of requests from a
+given IP address within a certain time period.
+
+There are established patterns and libraries for doing this. The most popular
+and well maintained library for express is
+[express-rate-limit](https://npm.im/express-rate-limit).
+
+One challenge with rate limiting in a production environment is if you have
+multiple instances of your application running behind a load balancer (which in
+our case is Fly). In this case, you need to ensure that the rate limit is
+applied across all instances and not just to each individual instance.
+`express-rate-limit` allows you to do this by using a shared storage mechanism.
+A common solution is Redis or memcached.
+
+Rate limiting doesn't completely eliminate the problem of triggering unsolicited
+emails (CSRF tokens will do an even better job of reducing those) but it does
+help a great deal.
+
+Another thing to consider is what the rate limit levels will be. The entire
+application should not necessarily have the same rate limit. Users of web
+applications will often perform many more GET requests than they do POST
+requests for example. So some endpoints and methods will require "stronger" rate
+limits than others.
+
+## Decision
+
+We will use `express-rate-limit` to rate limit requests to our application. We
+will also use the built-in memory storage mechanism as the default. This is good
+enough for many applications and is the simplest to implement. Evolving to a
+Redis based solution should not require a great deal of extra effort for folks
+requiring that additional level of protection.
+
+We'll have a stronger rate limit on non-GET requests in general and an even
+stronger rate limit on certain endpoints that are more likely to be abused.
+
+## Consequences
+
+This could mean that folks who are using the application from a shared IP
+address (such as a corporate network) may be rate limited more aggressively than
+we would like. This is a trade-off we are willing to make for now.
+
+Our default levels for rate limiting could also be overly aggressive for some
+people's use cases causing confusion. So we'll need to document this to help
+people be made aware of the potential issue and how to resolve it.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/026-path-aliases.md b/examples/federation/epic-stack-remote/docs/decisions/026-path-aliases.md
new file mode 100644
index 0000000..5d11cf3
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/026-path-aliases.md
@@ -0,0 +1,36 @@
+# Path Aliases
+
+Date: 2023-08-14
+
+Status: superseded by [031-imports](./031-imports.md)
+
+## Context
+
+It's pretty common to configure TypeScript to have path aliases for imports.
+This allows you to avoid relative imports and makes it easier to move files
+around without having to update imports.
+
+When the Epic Stack started, we used path imports that were similar to those in
+the rest of the Remix ecosystem: `#` referenced the `app/` directory. We added
+`tests/` to make it easier to import test utils.
+
+However, we've found that this is confusing for new developers. It's not clear
+what `#` means, and seeing `import { thing } from 'tests/thing'` is confusing. I
+floated the idea of adding another alias for `@/` to be the app directory and or
+possibly just moving the `#` to the root and having that be the only alias. But
+at the end of the day, we're using TypeScript which will prevent us from making
+mistakes and modern editors will automatically handle imports for you anyway.
+
+At first it may feel like a pain, but less tooling magic is better and editors
+can really help reduce the pain. Additionally, we have ESLint configured to sort
+imports for us so we don't have to worry about that either. Just let the editor
+update the imports and let ESLint sort them.
+
+## Decision
+
+Remove the path aliases from the `tsconfig`.
+
+## Consequences
+
+This requires updating all the imports that utilized the path aliases to use
+relative imports.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/027-toasts.md b/examples/federation/epic-stack-remote/docs/decisions/027-toasts.md
new file mode 100644
index 0000000..3474456
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/027-toasts.md
@@ -0,0 +1,36 @@
+# Toasts
+
+Date: 2023-08-14
+
+Status: accepted
+
+## Context
+
+In the Epic Stack we used the Shadcn toast implementation. This worked ok, but
+it did require a lot of custom code for ourselves and did a poor job of managing
+multiple toast messages.
+
+We also had a shared `flash` session implementation for both toasts and
+confetti. This was overly complex.
+
+There's another library
+[someone told me about](https://twitter.com/ayushverma1194/status/1674848096155467788)
+that is a better fit. It's simpler and has an API sufficient to our use cases.
+
+It's also sufficiently customizable from a design perspective as well. And it's
+actively developed.
+
+## Decision
+
+Remove our own toast implementation and use the library instead.
+
+Also separate the toast and confetti session implementations. Toasts can
+continue to use a regular session, but confetti will be a much simpler cookie.
+
+## Consequences
+
+This will limit the level of customizability because we're now relying on a
+library for managing toast messages, however it also reduces the maintenance
+burden for users of the Epic Stack.
+
+This will also simplify the confetti implementation.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/028-permissions-rbac.md b/examples/federation/epic-stack-remote/docs/decisions/028-permissions-rbac.md
new file mode 100644
index 0000000..538ad72
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/028-permissions-rbac.md
@@ -0,0 +1,84 @@
+# Permissions (RBAC)
+
+Date: 2023-08-14
+
+Status: accepted
+
+## Context
+
+Originally, the Epic Stack had a `role` and `permission` model which was quite
+limited in its use case. It was not very useful and not based on any real world
+scenario:
+
+```prisma
+model Role {
+ id String @id @unique @default(cuid())
+ name String @unique
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ users User[]
+ permissions Permission[]
+}
+
+model Permission {
+ id String @id @unique @default(cuid())
+ name String @unique
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ roles Role[]
+}
+```
+
+There are various ways to implement permissions, but a common approach is called
+[Role Based Access Control (RBAC)](https://auth0.com/intro-to-iam/what-is-role-based-access-control-rbac).
+This is a very flexible approach and can be used in many different ways. As a
+more established approach it's also easier to find resources to learn about and
+understand it.
+
+## Decision
+
+We're changing the implementation to follow a RBAC model:
+
+```prisma
+model Permission {
+ id String @id @default(cuid())
+ action String // e.g. create, read, update, delete
+ entity String // e.g. note, user, etc.
+ access String // e.g. own or any
+ description String @default("")
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ roles Role[]
+
+ @@unique([action, entity, access])
+}
+
+model Role {
+ id String @id @default(cuid())
+ name String @unique
+ description String @default("")
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ users User[]
+ permissions Permission[]
+}
+```
+
+This allows us to have much more fine grained control over our permissions.
+Additionally, we can create utilities for determining whether a user has
+permission to perform an action and disallow them from doing so if they do not.
+
+## Consequences
+
+This is a breaking change for the Epic Stack. Anyone wanting to adopt this
+permissions model will need to perform a database migration. However, it's
+important that we make this change now because the previous model was not great.
+This one is.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/029-remix-auth.md b/examples/federation/epic-stack-remote/docs/decisions/029-remix-auth.md
new file mode 100644
index 0000000..86e5926
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/029-remix-auth.md
@@ -0,0 +1,22 @@
+# Remix Auth
+
+Date: 2023-08-14
+
+Status: accepted
+
+## Context
+
+At the start of Epic Stack, we were using
+[remix-auth-form](https://github.com/sergiodxa/remix-auth-form) for our
+username/password auth solution. This worked fine, but it really didn't give us
+any value over handling the auth song-and-dance ourselves.
+
+## Decision
+
+Instead of relying on remix-auth for handling authenticating the user's login
+form submission, we'll manage it ourselves.
+
+## Consequences
+
+This mostly allows us to remove some code. However, we're going to be keeping
+remix auth around for GitHub Auth
diff --git a/examples/federation/epic-stack-remote/docs/decisions/030-github-auth.md b/examples/federation/epic-stack-remote/docs/decisions/030-github-auth.md
new file mode 100644
index 0000000..6eca20a
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/030-github-auth.md
@@ -0,0 +1,69 @@
+# GitHub Auth
+
+Date: 2023-08-14
+
+Status: accepted
+
+## Context
+
+Many applications require integrating with third party authentication providers.
+For this reason, we want to support the idea of "connections" as a built-in part
+of the Epic Stack.
+
+There are many different providers we could support, but many people need to
+support more than just one. By building things in a way that allows us to
+support more than just a single auth provider, it allows us to also make it easy
+to swap to a different provider as needed.
+
+Many auth providers support OAuth2, but increasingly, many are also supporting
+OpenID Connect. OpenID Connect is a layer on top of OAuth2 that provides a
+standardized way to get user information from the auth provider.
+
+Sadly, GitHub (a common popular auth provider for many developer-focused apps)
+does not support OpenID, however, by using
+[`remix-auth`](https://github.com/sergiodxa/remix-auth), we can easily support
+GitHub as a built-in implementation and allow people to swap it out for whatever
+OAuth2 or OIDC auth provider they have (if OIDC, they can use
+[web-oidc](https://github.com/sergiodxa/web-oidc)).
+
+## Decision
+
+We will update the database schema to support multiple auth providers with a
+model called `Connection`:
+
+```prisma
+model Connection {
+ id String @id @default(cuid())
+ providerName String
+ providerId String @unique
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
+ userId String
+
+ @@unique([providerName, providerId])
+ @@unique([providerId, userId])
+}
+```
+
+We'll also build the appropriate callback URL handlers and UI to allow people to
+manage their connections.
+
+## Consequences
+
+With third party auth, this means that users may not have passwords. So we'll
+need to handle that situation and allow users to onboard without the use of
+passwords. We'll also need to prevent them from deleting all their connections
+until they've created a password.
+
+There are a number of states for the user to be in within the callback as well
+which all will need to be considered. All of these states will be tested to
+ensure they continue to function properly as people tune things for their needs.
+
+Additionally, we'll need to account for the fact that some folks don't want to
+set up the GitHub login flow from the start (to keep in line with our
+[Minimize Setup Friction guiding principle](#app/guiding-principles.md)), so
+we'll have to make sure that the app still runs properly without GitHub auth
+configured.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/031-imports.md b/examples/federation/epic-stack-remote/docs/decisions/031-imports.md
new file mode 100644
index 0000000..ebd8f47
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/031-imports.md
@@ -0,0 +1,76 @@
+# Imports
+
+Date: 2023-08-16
+
+Status: accepted
+
+## Context
+
+Recently, we removed the `~/*` and `tests/*` aliases in favor of relative
+imports. The arguments for this are described in
+[026-path-aliases](./026-path-aliases.md). While the arguments are sound, the
+big challenge with this approach is the fact that there are some times where you
+need to type out the import for something and doing that is a huge pain with
+relative routes.
+
+The issue is the fact that you can choose one of these options:
+
+1. Very flat files
+2. Long relative imports
+3. Path aliases
+
+Keeping files flat is just not a great option because it requires exceedingly
+long filenames for longer routes and it makes it hard to find files. Long
+relative imports are just a pain to type out and they are hard to read, copy,
+and manually modify.
+
+Despite the magic of Path aliases, they are actually a standard `package.json`
+supported feature. Sort of.
+[The `"imports"` field](https://nodejs.org/api/packages.html#imports) in
+`package.json` allows you to configure aliases for your imports. It's not
+exactly the same as TypeScript Path aliases, and using them doesn't give you
+autocomplete with TypeScript
+([yet](https://github.com/microsoft/TypeScript/pull/55015)), but if you
+configure both, then you can get the best of both worlds!
+
+By using the `"imports"` field, you don't have to do any special configuration
+for `vitest` or `eslint` to be able to resolve imports. They just resolve them
+using the standard.
+
+And by using the `tsconfig.json` `paths` field configured in the same way as the
+`"imports"` field, you get autocomplete and type checking for your imports. This
+should hopefully be temporary until TypeScript supports the `"imports"` field
+directly.
+
+One interesting requirement for `imports` is that they _must_ start with the `#`
+character to disambiguate from other imports. This is a bit annoying, but it's
+something that's not difficult to get used to. They also _must not_ start with
+`#/`. So you have to do `#app` instead of `#/app`. This is also a bit odd, but
+again it's just a matter of familiarity. So it's no big deal.
+
+## Decision
+
+We're going to configure `"imports"` in the `package.json` and `paths` in the
+`tsconfig.json` to use path aliases for imports.
+
+We'll set it to `"#*": "./*"` which will allow us to import anything in the root
+of the repo with `#/`.
+
+## Consequences
+
+This is unfortunately _very_ soon after making the decision to drop the alias.
+But I see this as slightly different because we're only using the alias to make
+up for a shortcoming in TypeScript temporarily. Once TypeScript supports the
+`"imports"` field, we can drop the `paths` field and just use the `"imports"`
+standard for Node.js.
+
+If someone wants to use the Epic Stack without Node.js, and their runtime
+doesn't support `package.json` imports (I'm not sure whether other runtimes do
+or not) they'll have to continue using the paths configuration. But that's not a
+consideration here.
+
+## Credits
+
+Big thank you to
+[Mateusz BurzyΕski](https://twitter.com/AndaristRake/status/1691807097078317287)
+for helping with this one!
diff --git a/examples/federation/epic-stack-remote/docs/decisions/032-csrf.md b/examples/federation/epic-stack-remote/docs/decisions/032-csrf.md
new file mode 100644
index 0000000..182f767
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/032-csrf.md
@@ -0,0 +1,33 @@
+# Cross-Site Request Forgery
+
+Date: 2023-10-11
+
+Status: superseded by [035](./035-remove-csrf.md)
+
+## Context
+
+You can learn all about Cross-Site Request Forgery from
+[EpicWeb.dev's forms workshop](https://forms.epicweb.dev/07). The TL;DR idea is
+that a malicious adversary can trick a user into making a request to your server
+that they did not intend to make. This can be used to make requests to your
+server that can do anything that the user can do.
+
+To defend against this attack, we need to ensure that the request is coming from
+a page that we control. We do this by adding a CSRF token to the page and
+checking that the token is present in the request. The token is generated by our
+own server and stored in an HTTP-only cookie. This means that it can't be
+accessed by third parties, but it will be sent with every request to our server.
+We also send that same token within the form submission and then check that the
+token in the form matches the token in the cookie.
+
+Once set up, this is a fairly straightforward thing to do and there are great
+tools to help us do it (`remix-utils` specifically).
+
+## Decision
+
+We'll implement CSRF protection to all our authenticated forms.
+
+## Consequences
+
+This is a tiny bit invasive to the code, but it doesn't add much complexity.
+It's certainly worth the added security.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/033-honeypot.md b/examples/federation/epic-stack-remote/docs/decisions/033-honeypot.md
new file mode 100644
index 0000000..d906e50
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/033-honeypot.md
@@ -0,0 +1,35 @@
+# Honeypot Fields
+
+Date: 2023-10-11
+
+Status: accepted
+
+## Context
+
+You can learn all about Honeypot Fields from
+[EpicWeb.dev's forms workshop](https://forms.epicweb.dev/06). The TL;DR idea is
+spam bots go around the internet filling in forms all over the place in hopes of
+getting their spammy links on your site among other things. This causes extra
+load on your server and in some cases can cause you issues. For example, our
+onboarding process sends an email to the user. If a spam bot fills out the form
+with a random email address, we'll send an email to that address and cause
+confusion in the best case or get marked as spam in the worst case.
+
+Most of these spam bots are not very sophisticated and will fill in every field
+on the form (even if those fields are visually hidden). We can use this to our
+advantage by adding a field to the form that is visually hidden and then
+checking that it is empty when the form is submitted. If it is not empty, we
+know that the form was filled out by a spam bot and we can ignore it.
+
+There are great tools to help us accomplish this (`remix-utils` specifically).
+
+## Decision
+
+We'll implement Honeypot Fields to all our public-facing forms. Authenticated
+forms won't need this because they're not accessible to spam bots anyway.
+
+## Consequences
+
+This is a tiny bit invasive to the code, but it doesn't add much complexity.
+It's certainly worth the added benefits to our server (and email
+deliverability).
diff --git a/examples/federation/epic-stack-remote/docs/decisions/034-source-maps.md b/examples/federation/epic-stack-remote/docs/decisions/034-source-maps.md
new file mode 100644
index 0000000..f60b73b
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/034-source-maps.md
@@ -0,0 +1,24 @@
+# Source Maps
+
+Date: 2023-11-03
+
+Status: accepted
+
+## Context
+
+Read [016-source-maps](016-source-maps.md) to come up to speed on the context.
+
+Because of our built-in sentry support, we need to generate source maps, but we
+do not necessarily need to ship source maps to the client. Despite the arguments
+made in the original source map decision document, the benefit of shipping
+source maps over not shipping them is reduced thanks to Sentry. And the dangers
+are still present.
+
+## Decision
+
+Delete source maps after they've been uploaded to Sentry.
+
+## Consequences
+
+This will mean debugging a production application in the client will be really
+hard, but with Sentry properly configured it should definitely not be a problem.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/035-remove-csrf.md b/examples/federation/epic-stack-remote/docs/decisions/035-remove-csrf.md
new file mode 100644
index 0000000..854c81d
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/035-remove-csrf.md
@@ -0,0 +1,43 @@
+# Remove CSRF
+
+Date: 2024-01-29
+
+Status: accepted
+
+## Context
+
+Read more about the original CSRF decision in [032-csrf.md](./032-csrf.md).
+
+Modern browser support for `SameSite: Lax` and our use of that for all cookies
+means that cookies are not sent on cross-site requests. This means that CSRF
+protection is not needed for our cookies.
+
+There are however a few exceptions which motivated the original inclusion of
+CSRF:
+
+- GET requests are not protected by `SameSite: Lax` and so are vulnerable to
+ CSRF attacks. However, we do not have any GET endpoints that perform mutations
+ on the server. The only GET endpoints we have are for fetching data and so
+ there is no meaningful CSRF attack that could be performed.
+- The `POST /login` endpoint does not require cookies at all and so is
+ technically vulnerable to CSRF attacks. But anyone who could exploit this
+ endpoint would have to know the user's username and password anyway in which
+ case they could just log in as the user directly.
+
+With the addition of the honeypot field to prevent bots from submitting the
+login form, the lack of vulnerability due to the cookie configuration, and the
+fact that CSRF adds a bit of complexity to the code, it just doesn't seem worth
+it to keep CSRF tokens around.
+
+## Decision
+
+Remove CSRF tokens from the codebase.
+
+## Consequences
+
+If someone adds a GET request which does mutate state, then this could be an
+issue. However, a CSRF token could be added back for that specific mutation.
+Also, if the cookie configuration is changed from `Lax` to `None` (useful in
+various contexts, but certainly not a good default), then CSRF tokens would need
+to be added back. So we'll add a comment to the code for configuring the cookie
+mentioning this.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/036-vite.md b/examples/federation/epic-stack-remote/docs/decisions/036-vite.md
new file mode 100644
index 0000000..6af222e
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/036-vite.md
@@ -0,0 +1,46 @@
+# Adopting Vite
+
+Date: 2024-02-22
+
+Status: accepted
+
+## Context
+
+[The Remix Team has created a Vite Plugin](https://remix.run/blog/remix-vite-stable)
+and it is now stable. It can be used to replace the existing remix compiler. In
+Remix v3 the plugin will be the only supported way to build remix applications.
+
+Using vite also means we get better hot module replacement, a thriving ecosystem
+of tools, and shared efforts with other projects using vite.
+
+If we don't adopt vite, we'll be stuck on Remix v2 forever π Now that the vite
+plugin is stable, adopting vite is really the only way forward.
+
+That said, we currently have a few route modules that mix server-only utilities
+with server/client code. In vite, you cannot have any exported functions which
+use server-only code, so those utilities will need to be moved. Luckily, the
+vite plugin will fail the build if it finds any issues so if it builds, it
+works. Additionally, this will help us make a cleaner separation between server
+and server/client code which is a good thing.
+
+The simple rule is this: if it's a Remix export (like `loader`, or `action`)
+then it can be in the route. If it's our own utility export (like
+`requireRecentVerification` we had in the `/verify` route) then it needs to go
+in a `.server` file. To be clear, if you don't export it, then it's fine.
+Server-only utility functions are fine in routes. It just becomes a problem for
+remix when they are exported.
+
+An interesting exception to this is sever-only code in the `handle` export like
+the [`getSitemapEntries` function](https://github.com/nasa-gcn/remix-seo). For
+this, you need to use
+[`vite-env-only`](https://github.com/pcattori/vite-env-only).
+
+## Decision
+
+Adopt vite.
+
+## Consequences
+
+Almost everything is better. We have slightly more complicated rules around the
+server/client code separation, but for the most part that's better and there are
+fewer surprises.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/037-generated-internal-command.md b/examples/federation/epic-stack-remote/docs/decisions/037-generated-internal-command.md
new file mode 100644
index 0000000..3d1044f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/037-generated-internal-command.md
@@ -0,0 +1,44 @@
+# Generated Internal Command Env Var
+
+Date: 2024-06-19
+
+Status: accepted
+
+## Context
+
+There are use cases where your application needs to talk to itself over HTTP.
+One example of this is when a read-replica instance needs to trigger an update
+to the cache in the primary instance. This can be done by making an HTTP request
+to the primary instance.
+
+To secure this communication, we can use a secret token that is shared between
+the instances. This token can be stored in the environment variables of the
+instances.
+
+Originally, this token was manually generated once and set as a secret in the
+Fly app. This token was then used in the application code to authenticate the
+requests.
+
+However, this manual process is error-prone and can lead to security issues if
+the token is leaked.
+
+An alternative is to generate the token in the Dockerfile and set it as an
+environment variable in the Fly app. This way, the token is generated
+automatically and is unique for each deployment.
+
+One drawback to this is during the deployment process, an old replica might
+still be running with the old token. This can cause issues if the new replica is
+expecting the new token. However, this should be short-lived and it's also
+possible the read replica is running out-of-date code anyway so it may be to our
+benefit anyway.
+
+## Decision
+
+We will generate the internal command token in the Dockerfile and set it as an
+environment variable in the Fly app.
+
+## Consequences
+
+We'll need to remove the steps during initial setup and the documentation
+instructions. This will simplify the setup process and reduce the risk of
+security issues due to leaked tokens.
diff --git a/examples/federation/epic-stack-remote/docs/decisions/038-remove-cleanup-db.md b/examples/federation/epic-stack-remote/docs/decisions/038-remove-cleanup-db.md
new file mode 100644
index 0000000..070f478
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/038-remove-cleanup-db.md
@@ -0,0 +1,45 @@
+# Remove Cleanup DB
+
+Date: 2024-10-28
+
+Status: accepted
+
+## Context
+
+We have a utility called `cleanupDb` that removes all tables from the database
+except for prisma migration tables. The reference to prisma migration tables is
+unfortunate because those are an implementation detail that we should not have
+to think about.
+
+The goal of `cleanupDb` was to make it easy for tests to reset the database
+without having to run `prisma migrate reset` which is too slow for lower level
+tests.
+
+We also used `cleanupDb` in the seed file to reset the database before seeding
+it.
+
+However, after a lot of work on the tests, we found a much simpler solution to
+resetting the database between tests: simply copy/paste the `base.db` file
+(which is a fresh database) to `test.db` before each test. We were already doing
+this before all the tests. It takes nanoseconds and is much simpler.
+
+For the seed script, it's nice to have the database be completely reset when
+running `prisma db seed` (in fact, our seed expects the database to be empty),
+but you can get the same behavior as our current `seed` with a fresh database by
+running `prisma migrate reset` (which runs the seed script after resetting the
+database).
+
+It would be nice to ditch the implementation detail of prisma's tables, so we'd
+like to remove this utility.
+
+## Decision
+
+Remove the `cleanupDb` utility and update our CI to run `prisma migrate reset`
+instead of `prisma db seed`.
+
+## Consequences
+
+Running `prisma db seed` will fail because the seed script expects the database
+to be empty. We could address this by using upsert or something, but really
+people should just run `prisma migrate reset` to seed the database (which is
+effectively what we used to do before removing `cleanupDb`).
diff --git a/examples/federation/epic-stack-remote/docs/decisions/README.md b/examples/federation/epic-stack-remote/docs/decisions/README.md
new file mode 100644
index 0000000..72d0909
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/decisions/README.md
@@ -0,0 +1,7 @@
+# Decisions
+
+This directory contains all the decisions we've made for this starter template
+and serves as a record for whenever we wonder why certain decisions were made.
+
+Decisions in here are never final. But these documents should serve as a good
+way for someone to come up to speed on why certain decisions were made.
diff --git a/examples/federation/epic-stack-remote/docs/deployment.md b/examples/federation/epic-stack-remote/docs/deployment.md
new file mode 100644
index 0000000..eada04f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/deployment.md
@@ -0,0 +1,189 @@
+# Deployment
+
+When you first create an Epic Stack repo, it should take you through a series of
+questions to get your app setup and deployed. However, we'll document the steps
+here in case things don't go well for you or you decide to do it manually later.
+Here they are!
+
+## Deploying to Fly.io
+
+Prior to your first deployment, you'll need to do a few things:
+
+1. [Install Fly](https://fly.io/docs/getting-started/installing-flyctl/).
+
+ > **Note**: Try `flyctl` instead of `fly` if the commands below won't work.
+
+2. Sign up and log in to Fly:
+
+ ```sh
+ fly auth signup
+ ```
+
+ > **Note**: If you have more than one Fly account, ensure that you are signed
+ > into the same account in the Fly CLI as you are in the browser. In your
+ > terminal, run `fly auth whoami` and ensure the email matches the Fly
+ > account signed into the browser.
+
+3. Create two apps on Fly, one for staging and one for production:
+
+ ```sh
+ fly apps create [YOUR_APP_NAME]
+ fly apps create [YOUR_APP_NAME]-staging
+ ```
+
+ > **Note**: Make sure this name matches the `app` set in your `fly.toml`
+ > file. Otherwise, you will not be able to deploy.
+
+4. Initialize Git.
+
+ ```sh
+ git init
+ ```
+
+- Create a new [GitHub Repository](https://repo.new), and then add it as the
+ remote for your project. **Do not push your app yet!**
+
+ ```sh
+ git remote add origin
+ ```
+
+5. Add secrets:
+
+- Add a `FLY_API_TOKEN` to your GitHub repo. To do this, go to your user
+ settings on Fly and create a new
+ [token](https://web.fly.io/user/personal_access_tokens/new), then add it to
+ [your repo secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets)
+ with the name `FLY_API_TOKEN`.
+
+- Add a `SESSION_SECRET` and `HONEYPOT_SECRET` to your fly app secrets, to do
+ this you can run the following commands:
+
+ ```sh
+ fly secrets set SESSION_SECRET=$(openssl rand -hex 32) HONEYPOT_SECRET=$(openssl rand -hex 32) --app [YOUR_APP_NAME]
+ fly secrets set SESSION_SECRET=$(openssl rand -hex 32) HONEYPOT_SECRET=$(openssl rand -hex 32) --app [YOUR_APP_NAME]-staging
+ ```
+
+ > **Note**: If you don't have openssl installed, you can also use
+ > [1Password](https://1password.com/password-generator) to generate a random
+ > secret, just replace `$(openssl rand -hex 32)` with the generated secret.
+
+- Add a `ALLOW_INDEXING` with `false` value to your non-production fly app
+ secrets, this is to prevent duplicate content from being indexed multiple
+ times by search engines. To do this you can run the following commands:
+
+ ```sh
+ fly secrets set ALLOW_INDEXING=false --app [YOUR_APP_NAME]-staging
+ ```
+
+6. Create production database:
+
+ Create a persistent volume for the sqlite database for both your staging and
+ production environments. Run the following (feel free to change the GB size
+ based on your needs and the region of your choice
+ (`https://fly.io/docs/reference/regions/`). If you do change the region, make
+ sure you change the `primary_region` in fly.toml as well):
+
+ ```sh
+ fly volumes create data --region sjc --size 1 --app [YOUR_APP_NAME]
+ fly volumes create data --region sjc --size 1 --app [YOUR_APP_NAME]-staging
+ ```
+
+7. Attach Consul:
+
+- Consul is a fly-managed service that manages your primary instance for data
+ replication
+ ([learn more about configuring consul](https://fly.io/docs/litefs/getting-started/#lease-configuration)).
+
+ ```sh
+ fly consul attach --app [YOUR_APP_NAME]
+ fly consul attach --app [YOUR_APP_NAME]-staging
+ ```
+
+8. Commit!
+
+ The Epic Stack comes with a GitHub Action that handles automatically
+ deploying your app to production and staging environments.
+
+ Now that everything is set up you can commit and push your changes to your
+ repo. Every commit to your `main` branch will trigger a deployment to your
+ production environment, and every commit to your `dev` branch will trigger a
+ deployment to your staging environment.
+
+---
+
+### Optional: Email service setup
+
+Find instructions for this optional step in [the email docs](./email.md).
+
+### Optional: Error monitoring setup
+
+Find instructions for this optional step in
+[the error tracking docs](./monitoring.md).
+
+### Optional: Connecting to your production database
+
+Find instructions for this optional step in [the database docs](./database.md).
+
+### Optional: Seeding Production
+
+Find instructions for this optional step in [the database docs](./database.md).
+
+## Deploying locally using fly
+
+If you'd like to deploy locally, just run fly's deploy command:
+
+```
+fly deploy
+```
+
+## Deploying locally using docker/podman
+
+If you'd like to deploy locally by building a docker container image, you
+definitely can. For that you need to make some minimal changes to the Dockerfile
+located at other/Dockerfile. Remove everything from the line that says (#prepare
+for litefs) in "other/Dockerfile" till the end of file and swap with the
+contents below.
+
+```
+# prepare for litefs
+VOLUME /litefs
+ADD . .
+
+EXPOSE ${PORT}
+ENTRYPOINT ["/myapp/other/docker-entry-point.sh"]
+```
+
+There are 2 things that we are doing here.
+
+1. docker volume is used to swap out the fly.io litefs mount.
+2. Docker ENTRYPOINT is used to execute some commands upon launching of the
+ docker container
+
+Create a file at other/docker-entry-point.sh with the contents below.
+
+```
+#!/bin/sh -ex
+
+npx prisma migrate deploy
+sqlite3 /litefs/data/sqlite.db "PRAGMA journal_mode = WAL;"
+sqlite3 /litefs/data/cache.db "PRAGMA journal_mode = WAL;"
+npm run start
+```
+
+This takes care of applying the prisma migrations, followed by launching the
+node application (on port 8081).
+
+Helpful commands:
+
+```
+# builds the docker container
+docker build -t epic-stack . -f other/Dockerfile --build-arg COMMIT_SHA=`git rev-parse --short HEAD`
+
+# mountpoint for your sqlite databases
+mkdir ~/litefs
+
+# Runs the docker container.
+docker run -d -p 8081:8081 -e SESSION_SECRET='somesecret' -e HONEYPOT_SECRET='somesecret' -e FLY='false' -v ~/litefs:/litefs epic-stack
+
+# http://localhost:8081 should now point to your docker instance. ~/litefs directory has the sqlite databases
+```
diff --git a/examples/federation/epic-stack-remote/docs/email.md b/examples/federation/epic-stack-remote/docs/email.md
new file mode 100644
index 0000000..c42d80f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/email.md
@@ -0,0 +1,22 @@
+# Email
+
+This document describes how to get [Resend](https://resend.com) (the Epic Stack
+email provider) setup.
+
+> **NOTE**: this is an optional step. During development the emails will be
+> logged to the terminal and in production if you haven't set the proper
+> environment variables yet you will get a warning until you set the environment
+> variables.
+
+Create [an API Key](https://resend.com/api-keys) and set `RESEND_API_KEY` in
+both prod and staging:
+
+```sh
+fly secrets set RESEND_API_KEY="re_blAh_blaHBlaHblahBLAhBlAh" --app [YOUR_APP_NAME]
+fly secrets set RESEND_API_KEY="re_blAh_blaHBlaHblahBLAhBlAh" --app [YOUR_APP_NAME]-staging
+```
+
+Setup a [custom sending domain](https://resend.com/domains) and then make sure
+to update the `from` email address in `app/utils/email.server.ts` and the
+`expect(email.from).toBe` in `tests/e2e/onboarding.test.ts` to the one you want
+your emails to come from.
diff --git a/examples/federation/epic-stack-remote/docs/examples.md b/examples/federation/epic-stack-remote/docs/examples.md
new file mode 100644
index 0000000..0b43e02
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/examples.md
@@ -0,0 +1,146 @@
+# Examples
+
+In keeping with the [guiding principle](guiding-principles.md) to "Minimize
+Setup Friction," there are some things that may be pretty common for web
+applications to do, but aren't common enough to be included in the main
+template.
+
+This page links to examples of how to implement some things with the Epic Stack.
+
+- [Framer Motion](https://github.com/kentcdodds/epic-stack-with-framer-motion)
+ by [@kentcdodds](https://github.com/kentcdodds): Using client hints to avoid
+ content layout shift with `prefers-reduced-motion` and framer motion
+ animations.
+- [Cross-site Request Forgery Protection (CSRF)](https://github.com/epicweb-dev/epic-stack-example-csrf)
+ by [@kentcdodds](https://github.com/kentcdodds): An example of the Epic Stack
+ with CSRF protection on forms.
+- [Epic Stack + OpenAI](https://github.com/kentcdodds/epic-ai): by
+ [@kentcdodds](https://github.com/kentcdodds): An example of the Epic Stack
+ with OpenAI's GPT API (enhances the notes feature with "generate" buttons).
+- [Prisma Client Extensions](https://github.com/L-Steinmacher/epic-stack-with-prisma-client-extensions)
+ by
+ [@L-Steinmacher](https://github.com/L-Steinmacher/epic-stack-with-prisma-client-extensions):
+ An example of the Epic Stack with Prisma Client extensions activated for enum
+ like behavior in SQLite.
+- [Epic Stack + Storybook](https://github.com/moishinetzer/epic-stack-with-storybook):
+ by [@moishinetzer](https://github.com/moishinetzer): An example of the Epic
+ Stack with Storybook. It also showcases creating a Remix stub, which is very
+ helpful for isolating Remix-specific components inside of Storybook.
+- [Socket.IO](https://github.com/L-Steinmacher/epic-stack-with-socket.io): by
+ [@L-Steinmacher](https://github.com/L-Steinmacher): An example of setting up
+ using websockets in the Epic Stack using the `Socket.IO` library.
+- [User Impersonation](https://github.com/alan2207/epic-stack-with-user-impersonation)
+ by [@alan2207](https://github.com/alan2207): An example Remix application
+ showcasing how to implement user impersonation in the Epic Stack.
+- [Epic Stack + Tailwind CSS Plugin](https://github.com/hakimLyon/epic-stack-with-tailwind-css-plugin)
+ by [@hakimLyon](https://github.com/hakimLyon): An example of the Epic Stack
+ with Tailwind CSS Plugin.
+- [Epic Stack + GitHub Auth](https://github.com/kentcdodds/epic-github-auth) by
+ [@kentcdodds](https://github.com/kentcdodds): An example of the Epic Stack
+ with GitHub Auth.
+- [Epic Stack + MongoDB as the Database](https://github.com/hakimLyon/epic-stack-with-prisma-mongodb)
+ by [@hakimLyon](https://github.com/hakimLyon): An example of the Epic Stack
+ with Prisma using MongoDB as the database.
+- [Epic Stack Custom Themes](https://github.com/kiliman/epic-stack-theme) by
+ [@kiliman](https://github.com/kiliman): An example showing how to create a
+ custom theme using the
+ [`shadcn-custom-theme`](https://github.com/kiliman/shadcn-custom-theme) tool.
+- [Epic Stack + OpenID Connect Auth (Google)](https://github.com/kentcdodds/epic-oidc)
+ by [@kentcdodds](https://github.com/kentcdodds): An example of the Epic Stack
+ with OpenID Connect Auth (Google) using [web-oidc](https://npm.im/web-oidc)
+ and [remix-auth](https://npm.im/remix-auth).
+- [Epic Stack + Fathom Analytics](https://github.com/xstevenyung/epic-stack-with-fathom-analytics)
+ by [@xstevenyung](https://github.com/xstevenyung): An example of the Epic
+ Stack with Fanthom Analytics via CDN
+- [Epic Stack + Tenant Users](https://github.com/offseat/epic-stack-tenant) by
+ [@vinstah](https://github.com/vinstah): An example of the Epic Stack with
+ tenant users and members starter
+- [Epic Stack + i18n](https://github.com/rperon/epic-stack-with-i18n/) by
+ [@rperon](https://github.com/rperon): An example of the Epic Stack with i18n
+ using [i18next](https://www.i18next.com/) and
+ [remix-18next](https://github.com/sergiodxa/remix-i18next)
+- [Epic Stack + Argos](https://github.com/jsfez/epic-stack-with-argos) by
+ [@jsfez](https://github.com/jsfez): An example of the Epic Stack with
+ [Argos](https://www.argos-ci.com/) for visual testing
+- [Epic Stack with Nx](https://github.com/isaacplmann/epic-stack-with-nx): An
+ example of using [Nx](https://nx.dev) in an Epic Stack app
+- [Epic Stack monorepo with pnpm + turbo](https://github.com/abetoots/epic-stack-turborepo):
+ An example of the Epic Stack in a monorepo setup
+- [Epic Stack + passkeys/webauthn](https://github.com/rperon/epic-stack-with-passkeys/)
+ by [@rperon](https://github.com/rperon): An example of the Epic Stack with
+ passkeys using
+ [remix-auth-webauthn](https://github.com/alexanderson1993/remix-auth-webauthn)
+ and [remix-auth](https://npm.im/remix-auth).
+- [Epic Stack with jsx-email](https://github.com/djhi/epic-stack-jsx-email): An
+ example of the Epic Stack that uses [jsx-email](https://jsx.email/) instead of
+ [react-email](https://react.email/)
+- [Epic Stack with Query](https://github.com/gc-victor/epic-stack-with-query) by
+ [@gc-victor](https://github.com/gc-victor): An example of the Epic Stack using
+ [Query](https://github.com/gc-victor/query). Using Epic Stack with Query, you
+ can access the databases directly from your application while enjoying a range
+ of benefits from Query to manage your databases, including
+ [Query CLI](https://github.com/gc-victor/query/blob/main/README.md#cli),
+ [Query API](https://github.com/gc-victor/query/blob/main/README.md#apis),
+ [Query Funcions](https://github.com/gc-victor/query?tab=readme-ov-file#function)
+ and [Query Studio](https://github.com/gc-victor/query-studio).
+- [`has-js` class](https://github.com/epicweb-dev/epic-stack-example-has-js-class)
+ by [@kentcdodds](https://github.com/kentcdodds): An example of adding a
+ `has-js` class on the `` element so you can use that to easily display
+ different content for users who do not have JavaScript enabled.
+- [Confetti](https://github.com/kentcdodds/epic-stack-example-confetti) by
+ [@kentcdodds](https://github.com/kentcdodds): Display confetti when the user
+ does something worthy of a celebration.
+- [Epic Stack Border animation](https://github.com/hakimLyon/epic-stack-with-css-border-animation)
+ by [@hakimLyon](https://github.com/hakimLyon): An example of Epic Stack with a
+ CSS border animation.
+- [Epic Stack with Keystatic](https://github.com/simonswiss/epic-stack-with-keystatic)
+ by [@simonswiss](https://github.com/simonswiss): An example adding a MDX blog,
+ editable from an elegant Admin UI with Keystatic.
+- [Epic Stack + Ariakit](https://github.com/gnapse/epic-stack) by
+ [@gnapse](https://github.com/gnapse): A fork of the Epic Stack that uses
+ [Ariakit](https://ariakit.org/) instead of Radix UI.
+- [Epic Stack + Stripe Payments](https://github.com/saurabhp75/epic-stripe) by
+ [@saurabhp75](https://github.com/saurabhp75): An example of Epic Stack with
+ [Stripe](https://ariakit.org/) payments.
+- [Epic Stack + Remix PWA](https://github.com/ShafSpecs/offline-epic-stack) by
+ [@ShafSpecs](https://github.com/ShafSpecs): An example of using Epic Stack
+ offline with [Remix PWA](https://remix-pwa.run). The guide can be found
+ [here](https://remix-pwa.run/docs/main/offline)
+- [Stripped down Epic Stack for content sites](https://github.com/arpitdalal/epic-content-stack)
+ by [@arpitdalal](https://github.com/arpitdalal): An example of stripped down
+ Epic Stack without DB, E2E, and authentication code. It's suitable for content
+ sites that don't need a DB or authentication.
+- [Epic Stack + Shadcn Themes](https://github.com/saurabhp75/epic-themes) by
+ [@saurabhp75](https://github.com/saurabhp75): An example of Epic Stack with
+ [Shadcn Themes](https://ui.shadcn.com/themes).
+- [Epic Stack Globe Animation](https://github.com/hakimLyon/epic-stack-github-globe)
+ by [@hakimLyon](https://github.com/hakimLyon): An example of Epic Stack with a
+ Globe animation as seen on GitHub's homepage.
+- [Epic Stack + Hono](https://github.com/hakimLyon/epic-stack-hono) by
+ [@hakimLyon](https://github.com/hakimLyon): An example of Epic Stack with
+ Hono.
+
+## How to contribute
+
+[](https://www.epicweb.dev/tips/contribute-an-epic-stack-example)
+
+Watch:
+[Contribute an Epic Stack Example](https://www.epicweb.dev/tips/contribute-an-epic-stack-example)
+
+You don't need permission to contribute an example. Feel free to create your own
+repository based on the Epic Stack and add whatever you like for your example.
+Here are some tips:
+
+1. Create an `init` commit as soon as you generate the project, before you make
+ any changes. That way people can look at the commit history of your example
+ and see what you've added.
+2. Update the `README.md` with some information about what your example is about
+ and call out any interesting things you've done.
+3. Add the tags "epic-stack" and "epic-stack-example" so it will appear on
+ [this page on GitHub](https://github.com/topics/epic-stack-example).
+4. It's not normally necessary to deploy your example to production (simply
+ comment out the deployment part of the GitHub workflow), but you can if you
+ like.
+
+Once you've made your repo, simply open a pull request to this page and add your
+example to the bottom of the list with a brief description.
diff --git a/examples/federation/epic-stack-remote/docs/features.md b/examples/federation/epic-stack-remote/docs/features.md
new file mode 100644
index 0000000..ab070ed
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/features.md
@@ -0,0 +1,55 @@
+# Features
+
+Here are a few things you get today:
+
+- [Remix](https://remix.run/) is the Web Framework of choice
+- [Fly app deployment](https://fly.io/) with [Docker](https://www.docker.com/)
+- Multi-region, distributed, production-ready
+ [SQLite Database](https://sqlite.org/) with
+ [LiteFS](https://fly.io/docs/litefs/).
+- Healthcheck endpoint for
+ [Fly backups region fallbacks](https://fly.io/docs/reference/configuration/#services-http_checks)
+- [Grafana](https://grafana.com/) Dashboards of the running app
+- [GitHub Actions](https://github.com/features/actions) with testing and deploy
+ on merge for both production and staging environments
+- Email/Password Authentication with
+ [cookie-based sessions](https://remix.run/utils/sessions#md-createcookiesessionstorage)
+- Two-Factor Authentication (2fa) with support for authenticator apps.
+- Transactional email with [Resend](https://resend.com/) and forgot
+ password/password reset support.
+- Progressively Enhanced and fully type safe forms with
+ [Conform](https://conform.guide/)
+- Database ORM with [Prisma](https://prisma.io/)
+- Role-based User Permissions.
+- Custom built image hosting
+- Caching via [cachified](https://npm.im/@epic-web/cachified): Both in-memory
+ and SQLite-based (with
+ [better-sqlite3](https://github.com/WiseLibs/better-sqlite3))
+- Styling with [Tailwind](https://tailwindcss.com/)
+- An excellent, customizable component library with
+ [Radix UI](https://www.radix-ui.com/)
+- End-to-end testing with [Playwright](https://playwright.dev/)
+- Local third party request mocking with [MSW](https://mswjs.io/)
+- Unit testing with [Vitest](https://vitest.dev/) and
+ [Testing Library](https://testing-library.com/) with pre-configured Test
+ Database
+- Code formatting with [Prettier](https://prettier.io/)
+- Linting with [ESLint](https://eslint.org/)
+- Static Types with [TypeScript](https://typescriptlang.org/)
+- Runtime schema validation with [zod](https://zod.dev/)
+- Error monitoring with [Sentry](https://sentry.io/welcome/)
+- Light/Dark/System mode (without a flash of incorrect theme)
+
+Here are some things that will likely find their way into the Epic Stack (or the
+docs examples) in the future:
+
+- Logging
+- Ecommerce support with [Stripe](https://stripe.com/)
+- Ethical site analytics with [fathom](https://usefathom.com/)
+- Internationalization
+- Image optimization route and component
+- Feature flags
+- Documentation on production data seeding process
+
+Not a fan of bits of the stack? Fork it, change it, and use
+`npx create-remix --template your/repo`! Make it your own.
diff --git a/examples/federation/epic-stack-remote/docs/fonts.md b/examples/federation/epic-stack-remote/docs/fonts.md
new file mode 100644
index 0000000..a069f37
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/fonts.md
@@ -0,0 +1,152 @@
+# Fonts
+
+The Epic Stack uses the default font that comes with Tailwind CSS. This is a
+good default font but you may want to use a custom font for your site and it's
+easy to do so.
+
+## Using Custom Fonts
+
+You can use custom fonts by creating the `./public/fonts` directory (if it
+doesn't already exist) and adding them to it.
+[Google Fonts](https://fonts.google.com/) is a good place to find open source
+fonts. You will also need to add the `css` file for the font to the
+`./app/styles` directory, if your font doesn't come with one (Google Fonts
+don't) you can generate one using a tool like
+[Transfonter](https://transfonter.org/). Transfonter now has a fonts directory
+setting. Set that to `fonts` to have the `url` preset.
+
+Verify the `url` in the `css` is relative to the `public` folder. So it should
+look something like `url('/fonts/yourfont/yourfont-200.woff2')`.
+
+Now you've added your font, there's a few places you need to update to use it.
+
+1. Add your font to the CSS variables.
+
+ ```css
+ /* tailwind.css */
+ @layer base {
+ :root {
+ --font-sans: ;
+ }
+ }
+ ```
+
+2. Import the default theme and add your font to the `fontFamily` property.
+
+ ```ts
+ import defaultTheme from 'tailwindcss/defaultTheme.js'
+ // tailwind.config.ts
+ extend: {
+ ...extendedTheme,
+ fontFamily: {
+ sans: ['var(--font-sans)', ...defaultTheme.fontFamily.sans],
+ }
+ }
+
+ ```
+
+3. Import your font stylesheet.
+
+ ```tsx
+ // app/routes/root.tsx
+ import fontStyleSheetUrl from './styles/yourfont.css?url'
+ ```
+
+ Add the font stylesheet to the links array.
+
+ ```tsx
+ // app/routes/root.tsx
+ ...
+ { rel: 'preload', href: fontStyleSheetUrl, as: 'style' },
+ { rel: 'stylesheet', href: fontStyleSheetUrl },
+ ```
+
+4. Expose and cache your fonts folder.
+
+ ```ts
+ // server/index.ts
+ ...
+ app.use(
+ '/fonts',
+ // Can aggressively cache fonts as they don't change often
+ express.static('public/fonts', { immutable: true, maxAge: '1y' }),
+ )
+ ```
+
+That's it! You can now use your custom font should now be available to use in
+your site.
+
+## Font Metric Overrides
+
+When using custom fonts, your site elements may stretch or shrink to accommodate
+the font. This is because the browser doesn't know the dimensions of the font
+you're using until it arrives, which introduces Cumulative Layout Shift and
+impact its web vitals.
+
+In Epic Stack, we fixed this by introducing
+[Font Metric Overrides](https://github.com/epicweb-dev/epic-stack/pull/128/files).
+
+Follow the steps below to add Font Metric Overrides to your custom fonts.
+
+1. Generate the overrides for your font.
+
+ You can use [fontpie](https://www.npmjs.com/package/fontpie) utility to
+ generate the overrides. For each of your fonts, write the following in your
+ terminal:
+
+ ```bash
+ npx fontpie ./local/font/location.woff2 -w font-weight -s normal/italic -n YourFont
+ ```
+
+ #### Example
+
+ ```sh
+ npx fontpie ./public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-200.woff2 -w 200 -s normal -n NunitoSans
+ ```
+
+ ```css
+ @font-face {
+ font-family: 'NunitoSans Fallback';
+ font-style: normal;
+ font-weight: 200;
+ src: local('Arial');
+ ascent-override: 103.02%;
+ descent-override: 35.97%;
+ line-gap-override: 0%;
+ size-adjust: 98.13%;
+ }
+ ```
+
+ If you've got a lot of font files to override, you can use
+ [fontpie-from-css](https://github.com/matt-kinton/fontpie-from-css) to
+ generate the overrides from a CSS file.
+
+ ```sh
+ npx fontpie-from-css ./public/fonts/yourfont/yourfont.css
+ ```
+
+ **_Note:_** _If you've been following the steps above, you might have to copy
+ your `yourfont.css` file temporarily to the `./public` folder as
+ `fontpie-from-css` loads fonts relative to the CSS file._
+
+2. Add the overrides to your font stylesheet.
+
+ Use fontpie for every custom font used (including variants) or
+ fontpie-from-css and add the metric overrides to `yourfont.css`.
+
+ _Ensure the original font has the `font-display: swap` property or the
+ fallback wouldn't work!_
+
+3. Add the font fallback to the stylesheet.
+
+ ```css
+ /* tailwind.css */
+ @layer base {
+ :root {
+ --font-sans: ;
+ }
+ }
+ ```
+
+That's it! You can now use your custom font without worrying about Cumulative
+Layout Shift!
diff --git a/examples/federation/epic-stack-remote/docs/getting-started.md b/examples/federation/epic-stack-remote/docs/getting-started.md
new file mode 100644
index 0000000..bbecf01
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/getting-started.md
@@ -0,0 +1,60 @@
+# Getting Started with the Epic Stack
+
+The Epic Stack is a [Remix Stack](https://remix.run/stacks). To start your Epic
+Stack, run the following [`npx`](https://docs.npmjs.com/cli/v9/commands/npx)
+command:
+
+```sh
+npx create-epic-app@latest
+```
+
+This will prompt you for a project name (the name of the directory to put your
+project). Once you've selected that, the CLI will start the setup process.
+
+Once the setup is complete, go ahead and `cd` into the new project directory and
+run `npm run dev` to get the app started.
+
+Check the project README.md for instructions on getting the app deployed. You'll
+want to get this done early in the process to make sure you're all set up
+properly.
+
+If you'd like to skip some of the setup steps, you can set the following
+environment variables when you run the script:
+
+- `SKIP_SETUP` - skips running `npm run setup`
+- `SKIP_FORMAT` - skips running `npm run format`
+- `SKIP_DEPLOYMENT` - skips deployment setup
+
+So, if you enabled all of these it would be:
+
+```sh
+SKIP_SETUP=true SKIP_FORMAT=true SKIP_DEPLOYMENT=true npx create-epic-app@latest
+```
+
+Or, on windows:
+
+```
+set SKIP_SETUP=true && set SKIP_FORMAT=true && set SKIP_DEPLOYMENT=true && npx create-epic-app@latest
+```
+
+## Development
+
+- Initial setup:
+
+ ```sh
+ npm run setup
+ ```
+
+- Start dev server:
+
+ ```sh
+ npm run dev
+ ```
+
+This starts your app in development mode, rebuilding assets on file changes.
+
+The database seed script creates a new user with some data you can use to get
+started:
+
+- Username: `kody`
+- Password: `kodylovesyou`
diff --git a/examples/federation/epic-stack-remote/docs/guiding-principles.md b/examples/federation/epic-stack-remote/docs/guiding-principles.md
new file mode 100644
index 0000000..4e22222
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/guiding-principles.md
@@ -0,0 +1,30 @@
+# Epic Stack Guiding Principles
+
+Decisions about the Epic Stack should be guided by the following guiding
+principles:
+
+- **Limit Services:** If we can reasonably build, deploy, maintain it ourselves,
+ do it. Additionally, if we can reasonably run it within our app instance, do
+ it. This saves on cost and reduces complexity.
+- **Include Only Most Common Use Cases:** As a project generator, it is expected
+ that some code will necessarily be deleted, but implementing support for every
+ possible type of feature is literally impossible. _The starter app is not
+ docs_, so to demonstrate a feature or give an example, put that in the docs
+ instead of in the starter app.
+- **Minimize Setup Friction:** Try to keep the amount of time it takes to get an
+ app to production as small as possible. If a service is necessary, see if we
+ can defer signup for that service until its services are actually required.
+ Additionally, while the target audience for this stack is apps that need scale
+ you have to pay for, we try to fit within the free tier of any services used
+ during the exploration phase.
+- **Optimize for Adaptability:** While we feel great about our opinions,
+ ever-changing product requirements sometimes necessitate swapping trade-offs.
+ So while we try to keep things simple, we want to ensure teams using the Epic
+ Stack are able to adapt by switching between third party services to
+ custom-built services and vice-versa.
+- **Only one way:** Avoid providing more than one way to do the same thing. This
+ applies to both the pre-configured code and the documentation.
+- **Offline Development:** We want to enable offline development as much as
+ possible. Naturally we need to use third party services for some things (like
+ email), but for those we'll strive to provide a way to mock them out for local
+ development.
diff --git a/examples/federation/epic-stack-remote/docs/icons.md b/examples/federation/epic-stack-remote/docs/icons.md
new file mode 100644
index 0000000..c0f8850
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/icons.md
@@ -0,0 +1,41 @@
+# Icons
+
+The Epic Stack uses SVG sprites for
+[optimal icon performance](https://benadam.me/thoughts/react-svg-sprites/).
+You'll find raw SVGs in the `./other/svg-icons` directory. These are then
+compiled into a sprite using the `npm run build:icons` script which generates
+the `icon.svg` file and an `icons.json` manifest file that allows Typescript to
+pick up the names of the icons.
+
+You can use [Sly](https://github.com/jacobparis-insiders/sly/tree/main/cli) to
+add new icons from the command line.
+
+To add the `trash`, `pencil-1`, and `avatar` icons, run:
+
+```sh
+npx sly add @radix-ui/icons trash pencil-1 avatar
+```
+
+If you don't specify the icons, Sly will show an interactive list of all the
+icons available in the `@radix-ui/icons` collection and let you select the ones
+you want to add.
+
+Sly has been configured in the Epic Stack to automatically add the icons to the
+`./other/svg-icons` directory and to run the `npm run build:icons` script
+automatically, so there are no extra steps to take. You can see the
+configuration in the `./other/sly/sly.json` file.
+
+The SVGs used by default in the Epic Stack come from
+[icons.radix-ui.com](https://icons.radix-ui.com/). You can download additional
+SVG icons from there, or provide your own. Once you've added new files in the
+directory, run `npm run build:icons` and you can then use the `Icon` component
+to render it. The `icon` prop is the name of the file without the `.svg`
+extension. We recommend using `kebab-case` filenames rather than `PascalCase` to
+avoid casing issues with different operating systems.
+
+Note that the `./other/build-icons.ts` file automatically removes `width` and
+`height` props from your SVGs to ensure they scale properly.
+
+By default, all the icons will have a height and width of `1em` so they should
+match the font-size of the text they're next to. You can also customize the size
+using the `size` prop.
diff --git a/examples/federation/epic-stack-remote/docs/managing-updates.md b/examples/federation/epic-stack-remote/docs/managing-updates.md
new file mode 100644
index 0000000..0c52dda
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/managing-updates.md
@@ -0,0 +1,178 @@
+# Managing updates
+
+## Updating Node.js
+
+The Epic Stack runs a long-running Node.js server. It defaults to the current
+active LTS version of node
+([read the Node.js version decision document](./decisions/021-node-version.md)).
+
+If you wish to change the Node.js version, you can do so by updating the
+`engines.node` property in the `package.json` file.
+
+```json
+{
+ "engines": {
+ "node": "20.3.1"
+ }
+}
+```
+
+Make certain you do not use a version range here because this is used in the
+`./other/build-server.ts` to compile the express server code.
+
+You will also want to update the `Dockerfile` to use the same version of Node.js
+as the `package.json` file.
+
+```diff
+- FROM node:18-bookworm-slim as base
++ FROM node:20.3.1-bookworm-slim as base
+```
+
+You'll find the
+[Node.js versions available on Docker Hub](https://hub.docker.com/_/node).
+
+## Within the Epic Stack
+
+When you create a new project with the Epic Stack, a bunch of code is generated
+for you. This code is completely yours and there is no way to update it other
+than making manual changes. This is both a good thing and a bad thing. It's good
+in the sense that you can tweak it to fit your specific use cases. But it's a
+challenge because as the Epic Stack gets improvements there's no way to get
+those automatically. You have to keep track of the improvements in the Epic
+Stack and make those updates yourself.
+
+You shouldn't feel compelled to keep up-to-date with the latest of the Epic
+Stack template. If what you're using is working fine for you then just keep
+going with it. Only adopt changes as you feel the need to do so. Feel free to
+peruse
+[the Epic Stack's commit history](https://github.com/epicweb-dev/epic-stack/commits/main)
+anytime you'd like to see what updates could be made to your project.
+
+Check the [epic-stack field in the package.json file](../package.json) to see
+what the date and commit hash were when you created your project.
+
+## How to update NPM dependencies
+
+Another part of the Epic Stack is the dependencies of the project. These you
+will also have to keep up-to-date yourself, but there is a bit of an automated
+process to help you.
+
+Itβs important to update your packages to get new features, bug fixes, and
+security patches.
+[NPM Check Updates](https://www.npmjs.com/package/npm-check-updates) is a CLI
+that will help you safely make those updates. You can watch
+[this youtube video](https://www.youtube.com/watch?v=0XQXGx3lLaU) for a
+demonstration of how to do this.
+
+### See a list of packages that can be updated
+
+NPM packages follow [semantic versioning](https://semver.org). This command will
+show you which packages can be updated and which major, minor, or patch versions
+are available.
+
+```sh
+npx npm-check-updates
+```
+
+Notice the colors:
+
+- Green = (non-major version zero) patch updates
+- Cyan = minor updates
+- Red = major or [version zero (0.y.z)](https://semver.org/#spec-item-4) updates
+
+### Update green patch versions first, all at once
+
+Since green patch version updates are meant for backward-compatible bug fixes,
+it's ok to update them all at once.
+
+```sh
+npx npm-check-updates -u --target patch
+...
+npm i
+```
+
+> Note: `npx npm-check-updates -u -t patch` updates all patch versions,
+> including major version zero patch versions, which can break your code. If all
+> your patch updates are green, feel free to use this command instead to update
+> them all at once.
+
+Assuming package maintainers follow semantic versioning, updating patch versions
+shouldn't break anything, but it's good practice to re-run your tests before
+committing these changes.
+
+```sh
+npm run test -- run
+npm run test:e2e:run
+```
+
+If all tests pass, commit your changes.
+
+```sh
+git add .
+git commit -m "Updated patch versions"
+```
+
+### Update cyan minor versions second, one by one
+
+Minor version updates introduce new features in a backward-compatible way. This
+is exciting and it's good practice to take some time to explore the new
+functionality and apply relevant updates to your code base or plan to apply them
+later. It's recommended you do this package by package instead of all at once.
+
+To check for the new package's features, check its release notes on GitHub.
+
+> If you haven't updated a fairly active package in a while, reading all its
+> release notes can take some time. Take into consideration how important a
+> package is for your project when choosing which to update first.
+
+```sh
+npx npm-check-updates -u --filter
+npm i
+```
+
+Again, assuming package maintainers follow semantic versioning updating patch
+versions shouldn't break anything, but it's good practice to re-run your tests
+to make sure.
+
+```sh
+npm run test -- run
+npm run test:e2e:run
+```
+
+If all tests pass, commit your changes.
+
+```sh
+git add .
+git commit -m "Updated minor versions"
+```
+
+### Update red versions third, one by one
+
+Red updates can happen on patch or minor versions (for zero major version
+(0.y.z) packages) or major versions. Either way, they could be breaking changes.
+It's recommended you read its release notes to see what changed and plan
+accordingly.
+
+> Again, you might want to take into consideration how important a package is
+> for your project when choosing which to update first.
+
+```sh
+npx npm-check-updates -u -f
+npm i
+```
+
+Make sure you've made all relevant changes and that the tests pass.
+
+```sh
+npm run test -- run
+npm run test:e2e:run
+```
+
+If all tests pass, commit your changes.
+
+```sh
+git add .
+git commit -m "Updated major version"
+```
+
+Then continue for each package.
diff --git a/examples/federation/epic-stack-remote/docs/memory.md b/examples/federation/epic-stack-remote/docs/memory.md
new file mode 100644
index 0000000..f8389b9
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/memory.md
@@ -0,0 +1,17 @@
+# Memory
+
+Epic Stack apps start with a single instance with 256MB of memory. This is a
+pretty small amount of memory, but it's enough to get started with. To help
+avoid memory pressure even at that scale, we allocate a 512MB swap file. Learn
+more about this decision in
+[the memory swap decision document](decisions/010-memory-swap.md).
+
+To modify or increase the swap file, check `.swap_size_mb` in `fly.toml`. This
+file is executed before running our app within the `litefs.yml` config.
+
+> **NOTE**: PRs welcome to document how to determine the effectiveness of the
+> swap file for your app.
+
+To increase the memory allocated to your vm, use the
+[`fly scale`](https://fly.io/docs/flyctl/scale-memory/) command. You can
+[learn more about memory sizing in the Fly docs](https://fly.io/docs/machines/guides-examples/machine-sizing).
diff --git a/examples/federation/epic-stack-remote/docs/monitoring.md b/examples/federation/epic-stack-remote/docs/monitoring.md
new file mode 100644
index 0000000..ec354fc
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/monitoring.md
@@ -0,0 +1,67 @@
+# Monitoring
+
+This document describes how to get [Sentry](https://sentry.io/) (the Epic
+application monitoring provider) set up for error, performance, and replay
+monitoring.
+
+> **NOTE**: this is an optional step and only needed if you want monitoring in
+> production.
+
+## SaaS vs Self-Hosted
+
+Sentry offers both a [SaaS solution](https://sentry.io/) and
+[self-hosted solution](https://develop.sentry.dev/self-hosted/). This guide
+assumes you are using SaaS but the guide still works with self-hosted with a few
+modifications.
+
+## Signup
+
+You can sign up for Sentry and create a Remix project from visiting
+[this url](https://sentry.io/signup/?project_platform=javascript-remix) and
+filling out the signup form.
+
+## Setting up the sentry-vite plugin
+
+Once you see the onboarding page which has the DSN, copy that somewhere (this
+becomes `SENTRY_DSN`). Now, set the sentry dsn secret in Fly.io to be used as an
+env var during runtime:
+
+```sh
+fly secrets set SENTRY_DSN=
+```
+
+See the guides for React Router v7
+[here(library)](https://docs.sentry.io/platforms/javascript/guides/react/features/react-router/v7/)
+and
+[here(framwork)](https://docs.sentry.io/platforms/javascript/guides/react-router/).
+Note that the dedicated SDK for React Router is under development and features
+are lacking.
+
+To generate the auth token, click
+[this](https://sentry.io/orgredirect/settings/:orgslug/developer-settings/new-internal/)
+to create an internal integration (which grants the selected capabilities to the
+recipient, similar to how RBAC works). Give it a name and add the scope for
+`Releases:Admin` and `Organization:Read`. Press Save, and then generate the auth
+token at the bottom of the page under "Tokens", and copy that to a secure
+location (this becomes `SENTRY_AUTH_TOKEN`). Then visit the organization general
+settings page and copy the organization slug (`SENTRY_ORG`), and the slug name
+for your project under `Organization > Projects > Project > Name`
+(`SENTRY_PROJECT`).
+
+In the 'build' section of the [Dockerfile](../other/Dockerfile), there is an
+example of how to pass `SENTRY_AUTH_TOKEN` secret, so it is available to Vite
+when `npm run build` is run. You may also uncomment and hard code your
+`SENTRY_ORG` and `SENTRY_PROJECT` values. Setup up your secrets in
+[GitHub Actions secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions).
+You can do the same for any other secret (environment variable) you need at
+build time, just make sure those secrets (variables) are available on the CI
+runner: see the 'deploy' job from [`deploy`](../.github/workflows/deploy.yml)
+workflow. Note that these do not need to be added to the
+[`env.server`](../app/utils/env.server.ts) env vars schema, as they are only
+used during the build and not the runtime.
+
+The Sentry Vite plugin in [`vite.config.ts`](../vite.config.ts) will create
+sentry releases for you and automatically associate commits during the vite
+build once the `SENTRY_AUTH_TOKEN` is set. In this setup we have utilized a
+simple strategy for naming releases of using the commit sha, passed in as a
+build arg via the GitHub action workflow.
diff --git a/examples/federation/epic-stack-remote/docs/permissions.md b/examples/federation/epic-stack-remote/docs/permissions.md
new file mode 100644
index 0000000..a60f78d
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/permissions.md
@@ -0,0 +1,42 @@
+# Permissions
+
+The Epic Stack's Permissions model takes after
+[Role-Based Access Control (RBAC)](https://auth0.com/intro-to-iam/what-is-role-based-access-control-rbac).
+Each user has a set of roles, and each role has a set of permissions. A user's
+permissions are the union of the permissions of all their roles (with the more
+permissive permission taking precedence).
+
+The default development seed creates fine-grained permissions that include
+`create`, `read`, `update`, and `delete` permissions for `user` and `note` with
+the access of `own` and `any`. The default seed also creates `user` and `admin`
+roles with the sensible permissions for those roles.
+
+You can combine these permissions in different ways to support different roles
+for different personas of users of your application.
+
+The Epic Stack comes with built-in utilities for working with these permissions.
+Here are some examples to give you an idea:
+
+```ts
+// server-side only utilities
+const userCanDeleteAnyUser = await requireUserWithPermission(
+ request,
+ 'delete:user:any',
+)
+const userIsAdmin = await requireUserWithRole(request, 'admin')
+```
+
+```ts
+// UI utilities
+const user = useUser()
+const userCanCreateTheirOwnNotes = userHasPermission(user, 'create:note:own')
+const userIsUser = userHasRole(user, 'user')
+```
+
+There is currently no UI for managing permissions, but you can use prisma studio
+for establishing these.
+
+## Seeding the production database
+
+Check [the deployment docs](./deployment.md) for instructions on how to seed the
+production database with the roles you want.
diff --git a/examples/federation/epic-stack-remote/docs/redirects.md b/examples/federation/epic-stack-remote/docs/redirects.md
new file mode 100644
index 0000000..3667332
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/redirects.md
@@ -0,0 +1,88 @@
+# Redirects
+
+We handle redirects in Express. Incoming requests are handled by Express before
+they pass through to Remix, so by redirecting earlier you improve performance.
+
+## HTTP to HTTPS
+
+We force all traffic to HTTPS automatically. That way, no part of your
+application is open to request interception. This does not affect localhost, as
+we use Fly's request headers for determining when to redirect.
+
+```ts
+app.use((req, res, next) => {
+ const proto = req.get('X-Forwarded-Proto')
+ const host = getHost(req)
+ if (proto === 'http') {
+ res.set('X-Forwarded-Proto', 'https')
+ res.redirect(`https://${host}${req.originalUrl}`)
+ return
+ }
+ next()
+})
+```
+
+## Remove trailing slashes
+
+We also remove trailing slashes automatically. A url like
+`https://example.com/foo/` is automatically redirected to
+`https://example.com/foo`.
+
+This is important for SEO reasons, as website crawlers (like Google) treat these
+as separate URLs and will consider them to be distinct pages with duplicate
+content.
+
+```ts
+app.use((req, res, next) => {
+ if (req.path.endsWith('/') && req.path.length > 1) {
+ const query = req.url.slice(req.path.length)
+ const safepath = req.path.slice(0, -1).replace(/\/+/g, '/')
+ res.redirect(301, safepath + query)
+ } else {
+ next()
+ }
+})
+```
+
+## www subdomains
+
+You can redirect root domain traffic to your www subdomain or vice versa.
+
+[DNS level redirects do not work with Fly](https://community.fly.io/t/how-to-redirect-from-non-www-to-www/5795/2).
+The recommended way to implement this redirect is in your application code.
+
+First, you will need to register SSL certificates for both the www and the root
+domain. You can
+[register an SSL certificate for your domains in the terminal](https://fly.io/docs/getting-started/working-with-fly-apps/#fly-io-and-custom-domains)
+or in your [Fly dashboard](https://fly.io/dashboard/) by navigating to your app
+and selecting "Certificates" on the sidebar.
+
+After registering the two SSL certificates, Fly will allow traffic from either
+source to hit your application, and then you can redirect to your preferred
+domain in code.
+
+**To redirect non-www traffic to www**
+
+```ts
+app.use((req, res, next) => {
+ const host = getHost(req)
+ if (!host.startsWith('www.')) {
+ return res.redirect(301, `https://www.${host}${req.url}`)
+ } else {
+ next()
+ }
+})
+```
+
+**To redirect www traffic to non-www:**
+
+```ts
+app.use((req, res, next) => {
+ const host = getHost(req)
+ if (host.startsWith('www.')) {
+ return res.redirect(301, `https://${host.slice(4)}${req.url}`)
+ } else {
+ next()
+ }
+})
+```
diff --git a/examples/federation/epic-stack-remote/docs/routing.md b/examples/federation/epic-stack-remote/docs/routing.md
new file mode 100644
index 0000000..98022aa
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/routing.md
@@ -0,0 +1,145 @@
+# Routing
+
+The Epic Stack uses file-based routing with Remix. However, it's not using the
+built-in routing convention of Remix and instead is using
+[remix-flat-routes](https://github.com/kiliman/remix-flat-routes) which is a
+special implementation of the Remix convention that adds a few features. You'll
+find it configured for the application in the `vite.config.js` file at the root
+of the app. Specifically "hybrid routes."
+
+We'll defer to the `remix-flat-routes` documentation for specifics, but an
+important thing for you to know as you get used to this convention is you can
+always run `npx remix routes` from the root of the app and it will output the
+routes of your application in a JSX-like output that will reveal the routes that
+will be generated based on your current file structure. Here's an example of the
+Epic Stack routes at the time of this writing:
+
+```
+app/routes
+βββ _auth+
+βΒ Β βββ forgot-password.tsx
+βΒ Β βββ login.tsx
+βΒ Β βββ logout.tsx
+βΒ Β βββ onboarding.tsx
+βΒ Β βββ reset-password.tsx
+βΒ Β βββ signup.tsx
+βββ _marketing+
+βΒ Β βββ about.tsx
+βΒ Β βββ index.tsx
+βΒ Β βββ logos
+βΒ Β βΒ Β βββ logos.ts
+βΒ Β βΒ Β βββ ...
+βΒ Β βββ privacy.tsx
+βΒ Β βββ support.tsx
+βΒ Β βββ tos.tsx
+βββ admin+
+βΒ Β βββ cache.tsx
+βΒ Β βββ cache_.lru.$cacheKey.ts
+βΒ Β βββ cache_.sqlite.$cacheKey.ts
+βΒ Β βββ cache_.sqlite.tsx
+βββ me.tsx
+βββ resources+
+βΒ Β βββ delete-image.test.tsx
+βΒ Β βββ delete-image.tsx
+βΒ Β βββ delete-note.tsx
+βΒ Β βββ file.$fileId.tsx
+βΒ Β βββ healthcheck.tsx
+βΒ Β βββ image-upload.tsx
+βΒ Β βββ login.tsx
+βΒ Β βββ note-editor.tsx
+βΒ Β βββ theme.tsx
+βββ settings+
+βΒ Β βββ profile.photo.tsx
+βΒ Β βββ profile.tsx
+βββ users+
+ βββ $username.tsx
+ βββ $username_+
+ βββ notes.$noteId.tsx
+ βββ notes.$noteId_.edit.tsx
+ βββ notes.index.tsx
+ βββ notes.new.tsx
+ βββ notes.tsx
+
+9 directories, 54 files
+```
+
+```tsx
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Basically, remix-flat-routes hybrid routing allows us to get the best of both
+worlds:
+
+- Colocation of routes to the code they use
+- Organized folder structure to keep routes together as needed
+
+If you're familiar with the Remix routing convention, just think of it this way,
+remix-flat-routes converts `+/` to `.`.
diff --git a/examples/federation/epic-stack-remote/docs/secrets.md b/examples/federation/epic-stack-remote/docs/secrets.md
new file mode 100644
index 0000000..9aca013
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/secrets.md
@@ -0,0 +1,37 @@
+# Secrets
+
+Managing secrets in the Epic Stack is done using environment variables and the
+`fly secrets` command.
+
+> **Warning**: It is very important that you do NOT hard code any secrets in the
+> source code. Even if your app source is not public, there are a lot of reasons
+> this is dangerous and in the epic stack we default to creating source maps
+> which will reveal your hard coded secrets to the public. Read more about this
+> in [the source map decision document](./decisions/016-source-maps.md).
+
+## Local development
+
+When you need to create a new secret, it's best to add a line to your
+`.env.example` file so folks know that secret is necessary. The value you put in
+here should be not real because this file is committed to the repository.
+
+To keep everything in line with the [guiding principle](./guiding-principles.md)
+of "Offline Development," you should also strive make it so whatever service
+you're interacting with can be mocked out using MSW in the `test/mocks`
+directory.
+
+You can also put the real value of the secret in `.env` which is `.gitignore`d
+so you can interact with the real service if you need to during development.
+
+## Production secrets
+
+To publish a secret to your production and staging applications, you can use the
+`fly secrets set` command. For example, if you were integrating with the `tito`
+API, to set the `TITO_API_SECRET` secret, you would run the following command:
+
+```sh
+fly secrets set TITO_API_SECRET=some_secret_value
+fly secrets set TITO_API_SECRET=some_secret_value --app [YOUR_STAGING_APP_NAME]
+```
+
+This will redeploy your app with that environment variable set.
diff --git a/examples/federation/epic-stack-remote/docs/security.md b/examples/federation/epic-stack-remote/docs/security.md
new file mode 100644
index 0000000..15a4759
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/security.md
@@ -0,0 +1,85 @@
+# Security
+
+The Epic Stack has several security measures in place to protect your users and
+yourself. This (incomplete) document, explains some of the security measures
+that are in place and how to use them.
+
+## Content Security Policy
+
+The Epic Stack uses a strict
+[Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP).
+This means that only resources from trusted sources are allowed to be loaded.
+However, by default, the CSP is set to `report-only` which means that the
+browser will report violations of the CSP without actually blocking the
+resource.
+
+This is to prevent new users of the Epic Stack from being blocked or surprised
+by the CSP by default. However, it is recommended to enable the CSP in
+`server/index.ts` by removing the `reportOnly: true` option.
+
+## Fly's Internal Network
+
+The Epic Stack uses [Fly](https://fly.io) for hosting. Fly has an internal
+network that allows you to connect services to each other without exposing them
+to the public internet. Only services within your organization have access to
+this network, and only accounts in your organization have access as well.
+
+When running multiple instances of the Epic Stack, your instances communicate
+with each other over this internal network. Most of this happens behind the
+scenes with the consul service that Fly manages for us.
+
+We also have an endpoint that allows instances to connect to each other to
+update the cache in the primary region. This uses internal URLs for that
+communication (via [`litefs-js`](https://github.com/fly-apps/litefs-js)), but as
+an added layer of security it uses a shared secret to validate the requests.
+
+> This could be changed if there's a way to determine if a request is coming
+> from the internal network. But I haven't found a way to do that yet. PRs
+> welcome!
+
+Outside of this, the Epic Stack does not access other first-party services or
+databases.
+
+## Secrets
+
+The currently recommended policy for managing secrets is to place them in a
+`.env` file in the root of the application (which is `.gitignore`d). There is a
+`.env.example` which can be used as a template for this file (and if you do not
+need to actually connect to real services, this can be used as
+`cp .env.example .env`).
+
+These secrets need to also be set on Fly using the `fly secrets` command.
+
+There are significant limitations to this approach and will probably be improved
+in the future.
+
+## [Cross-Site Scripting (XSS)](https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting)
+
+React has built-in support for XSS protection. It does this by escaping all
+values by default. This means that if you want to render HTML, you need to use
+the `dangerouslySetInnerHTML` prop. This is a good thing, but it does mean that
+you need to be careful when rendering HTML. Never pass anything that is
+user-generated to this prop.
+
+## [Cross-Site Request Forgery (CSRF)](https://forms.epicweb.dev/07)
+
+The Epic Stack has built-in support to prevent CSRF attacks. We use the
+[`remix-utils`](https://github.com/sergiodxa/remix-utils)
+[CSRF-related utilities](https://github.com/sergiodxa/remix-utils#csrf) to do
+this.
+
+## [Honeypot](https://forms.epicweb.dev/06)
+
+The Epic Stack has built-in support for honeypot fields. We use the
+[`remix-utils`](https://github.com/sergiodxa/remix-utils)
+[honeypot-related utilities](https://github.com/sergiodxa/remix-utils#form-honeypot)
+to do this.
+
+## Rate Limiting
+
+The Epic Stack uses a rate limiter to prevent abuse of the API. This is
+configured in the `server/index.ts` file and can be changed as needed. By
+default it uses [`express-rate-limit`](https://npm.im/express-rate-limit) with
+the in-memory store. There are trade-offs with this simpler approach, but it
+should be relatively simple to externalize the store into Redis as that's a
+built-in feature to express-rate-limit.
diff --git a/examples/federation/epic-stack-remote/docs/seo.md b/examples/federation/epic-stack-remote/docs/seo.md
new file mode 100644
index 0000000..b5de558
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/seo.md
@@ -0,0 +1,51 @@
+# SEO
+
+Remix has built-in support for setting up `meta` tags on a per-route basis which
+you can read about
+[in the Remix Metadata docs](https://remix.run/docs/en/main/route/meta).
+
+The Epic Stack also has built-in support for `/robots.txt` and `/sitemap.xml`
+via [resource routes](https://remix.run/docs/en/main/guides/resource-routes)
+using [`@nasa-gcn/remix-seo`](https://github.com/nasa-gcn/remix-seo). By
+default, all routes are included in the `sitemap.xml` file, but you can
+configure which routes are included using the `handle` export in the route. Only
+public-facing pages should be included in the `sitemap.xml` file.
+
+Here are two quick examples of how to customize the sitemap on a per-route basis
+from the `@nasa-gcn/remix-seo` docs:
+
+```tsx
+// routes/blog/_layout.tsx
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { serverOnly$ } from 'vite-env-only/macros'
+
+export const handle: SEOHandle = {
+ getSitemapEntries: serverOnly$(async (request) => {
+ const blogs = await db.blog.findMany()
+ return blogs.map((blog) => {
+ return { route: `/blog/${blog.slug}`, priority: 0.7 }
+ })
+ }),
+}
+```
+
+Note the use of
+[`vite-env-only/macros`](https://github.com/pcattori/vite-env-only). This is
+because `handle` is a route export object that goes in both the client as well
+as the server, but our sitemap function should only be run on the server. So we
+use `vite-env-only/macros` to make sure the function is removed for the client
+build. Support for this is pre-configured in the `vite.config.ts` file.
+
+```tsx
+// in your routes/url-that-doesnt-need-sitemap
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { type Route } from './+types/sitemap[.]xml.ts'
+
+export async function loader({ request }: Route.LoaderArgs) {
+ /**/
+}
+
+export const handle: SEOHandle = {
+ getSitemapEntries: () => null,
+}
+```
diff --git a/examples/federation/epic-stack-remote/docs/server-timing.md b/examples/federation/epic-stack-remote/docs/server-timing.md
new file mode 100644
index 0000000..a850934
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/server-timing.md
@@ -0,0 +1,89 @@
+# Server Timing
+
+
+
+The Epic Stack comes with a built-in server timing utility that allows you to
+measure the performance of your application. You can find it in the
+`app/utils/timing.server.ts` file. The idea is you can wrap a function in a
+`time` call and then use the timings object to generate a `Server-Timing` header
+which you can then use to have fine grained timing metrics for requests made in
+your app.
+
+You can
+[learn more about the Server Timing header on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing).
+The metrics passed in this header will be visually displayed in
+[the DevTools "Timing" tab](https://developer.chrome.com/docs/devtools/network/reference/#timing).
+
+## Usage
+
+Timings requires four parts:
+
+1. Setup Timings
+2. Time functions
+3. Create headers
+4. Send headers
+
+Here are all those parts in action in the `/user/:username/notes` route at the
+time of this writing:
+
+```tsx
+import {
+ combineServerTimings,
+ makeTimings,
+ time,
+} from '#app/utils/timing.server.ts'
+import { type Route } from './+types/notes.ts'
+
+export async function loader({ params }: Route.LoaderArgs) {
+ const timings = makeTimings('notes loader') // <-- 1. Setup Timings
+ // 2. Time functions
+ const owner = await time(
+ () =>
+ prisma.user.findUnique({
+ where: {
+ username: params.username,
+ },
+ select: {
+ id: true,
+ username: true,
+ name: true,
+ imageId: true,
+ },
+ }),
+ { timings, type: 'find user' },
+ )
+ if (!owner) {
+ throw new Response('Not found', { status: 404 })
+ }
+ // 2. Time functions
+ const notes = await time(
+ () =>
+ prisma.note.findMany({
+ where: {
+ ownerId: owner.id,
+ },
+ select: {
+ id: true,
+ title: true,
+ },
+ }),
+ { timings, type: 'find notes' },
+ )
+ return json(
+ { owner, notes },
+ { headers: { 'Server-Timing': timings.toString() } }, // <-- 3. Create headers
+ )
+}
+
+// We have a general headers handler to save you from boilerplating.
+export const headers: HeadersFunction = pipeHeaders
+// this is basically what it does though
+export const headers: Route.HeadersFunction = ({ loaderHeaders, parentHeaders }) => {
+ return {
+ 'Server-Timing': combineServerTimings(parentHeaders, loaderHeaders), // <-- 4. Send headers
+ }
+}
+```
+
+You can
+[learn more about `headers` in the React Router docs](https://reactrouter.com/how-to/headers)
diff --git a/examples/federation/epic-stack-remote/docs/testing.md b/examples/federation/epic-stack-remote/docs/testing.md
new file mode 100644
index 0000000..8a0b630
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/testing.md
@@ -0,0 +1,48 @@
+# Testing
+
+## Playwright
+
+We use Playwright for our End-to-End tests in this project. You'll find those in
+the `tests` directory. As you make changes, add to an existing file or create a
+new file in the `tests` directory to test your changes.
+
+To run these tests in development, run `npm run test:e2e:dev` which will start
+the dev server for the app and run Playwright on it.
+
+We have a fixture for testing authenticated features without having to go
+through the login flow:
+
+```ts
+test('my test', async ({ page, login }) => {
+ const user = await login()
+ // you are now logged in
+})
+```
+
+We also auto-delete the user at the end of your test. That way, we can keep your
+local db clean and keep your tests isolated from one another.
+
+## Vitest
+
+For lower level tests of utilities and individual components, we use `vitest`.
+We have DOM-specific assertion helpers via
+[`@testing-library/jest-dom`](https://testing-library.com/jest-dom).
+
+## Type Checking
+
+This project uses TypeScript. It's recommended to get TypeScript set up for your
+editor to get a really great in-editor experience with type checking and
+auto-complete. To run type checking across the whole project, run
+`npm run typecheck`.
+
+## Linting
+
+This project uses ESLint for linting. That is configured in `.eslintrc.js`.
+
+## Formatting
+
+We use [Prettier](https://prettier.io/) for auto-formatting in this project.
+It's recommended to install an editor plugin (like the
+[VSCode Prettier plugin](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode))
+to get auto-formatting on save. There's also a `npm run format` script you can
+run to format all files in the project.
diff --git a/examples/federation/epic-stack-remote/docs/timezone.md b/examples/federation/epic-stack-remote/docs/timezone.md
new file mode 100644
index 0000000..4234892
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/timezone.md
@@ -0,0 +1,37 @@
+# Timezones
+
+Server rendering timezones has always been a pain. This is because the server
+doesn't know the user's timezone. It only knows the timezone of the server. So
+lots of people will take the easy way out and do one of the following
+workarounds:
+
+- Just render in UTC: Not great because it's not the user's timezone
+- Render in the server's timezone: Not great because it's not the user's
+ timezone
+- Render in the server's timezone, and hydrate in the client's timezone: Not
+ great because it causes a flash of incorrect content (and a hydration error
+ unless you add `suppressHydrationWarning={true}` to the element)
+- Don't render the time on the server at all: Not great because it's a flash of
+ incomplete content (and no, fading it in does not count).
+- Only render the time from user interaction: Sometimes this is fine, but often
+ you're just compromising on UX and you know it.
+
+Thanks to the Epic Stack's built-in support for
+[client hints](./client-hints.md), we can do better! We have a client hint set
+up for the user's timezone. This means you can render the time on the server in
+the user's timezone, and hydrate it in the user's timezone, without any flash of
+incorrect content or hydration errors.
+
+You can use this in a few ways. In server-side only code,
+`getHints(request).timeZone` will be what you're looking for. In UI code, you
+can use `useHints().timeZone` to get the user's timezone.
+
+For the server-side code, we have a `getDateTimeFormat` utility uses to give you
+a
+[`DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat)
+object that is in the user's timezone (it also uses the standard
+`accept-language` header to determine the user's preferred locale).
+
+If you'd prefer to use a library for formatting dates and times, feel free to
+simply access the timezone from the hints and use it with your library of
+choice.
diff --git a/examples/federation/epic-stack-remote/docs/toasts.md b/examples/federation/epic-stack-remote/docs/toasts.md
new file mode 100644
index 0000000..ccba4da
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/toasts.md
@@ -0,0 +1,62 @@
+# Toasts
+
+Toast messages are great ways to temporarily call someone's attention to
+something. They are often used to notify users of a successful or failed action.
+
+
+
+There are utilities in the Epic Stack for toast notifications.
+
+This is managed by a special session using a concept called "flash data" which
+is a temporary session value that is only available for the next request. This
+is a great way to pass data to the next request without having to worry about
+the data persisting in the session. And you don't have to worry about managing
+state either. It all just lives in the cookie.
+
+The primary utility you'll use for redirecting with toast notifications is
+`redirectWithToast` from `app/utils/toast.server.ts`. Here's a simple example of
+using this:
+
+```tsx
+return redirectWithToast(`/users/${note.owner.username}/notes/${note.id}`, {
+ description: id ? 'Note updated' : 'Note created',
+})
+```
+
+This accepts an additional argument for other `ResponseInit` options so you can
+set other headers, etc.
+
+If you don't wish to redirect, you could use the underlying `createToastHeaders`
+directly:
+
+```tsx
+return json(
+ { success: true },
+ {
+ headers: await createToastHeaders({
+ description: 'Note updated',
+ type: 'success',
+ }),
+ },
+)
+```
+
+And if you need to set multiple headers, you can use the `combineHeaders`
+utility from `app/utils/misc.tsx`:
+
+```tsx
+return json(
+ { success: true },
+ {
+ headers: combineHeaders(
+ await createToastHeaders({
+ toast: {
+ description: 'Note updated',
+ type: 'success',
+ },
+ }),
+ { 'x-foo': 'bar' },
+ ),
+ },
+)
+```
diff --git a/examples/federation/epic-stack-remote/docs/troubleshooting.md b/examples/federation/epic-stack-remote/docs/troubleshooting.md
new file mode 100644
index 0000000..db0c828
--- /dev/null
+++ b/examples/federation/epic-stack-remote/docs/troubleshooting.md
@@ -0,0 +1,76 @@
+# Troubleshooting
+
+This is the page where we document common errors and how to fix them:
+
+## Content Security Policy violations
+
+If you've received an error like this:
+
+> Refused to load the image 'https://example.com/thing.png' because it violates
+> the following Content Security Policy directive: "img-src 'self'".
+
+This means you're trying to add a link to a resource that is not allowed. Learn
+more about the decision to add this content security policy (CSP) in
+[the decision document](./decisions/008-content-security-policy.md). NOTE: This
+is disabled by default as of
+[the report-only CSP decision](./decisions/022-report-only-csp.md). It is,
+however, recommended to be enabled for security reasons.
+
+To fix this, adjust the CSP to allow the resource you're trying to add. This can
+be done in the `server/index.ts` file.
+
+```diff
+ contentSecurityPolicy: {
+ directives: {
+ 'connect-src': [
+ MODE === 'development' ? 'ws:' : null,
+ process.env.SENTRY_DSN ? '*.sentry.io' : null,
+ "'self'",
+ ].filter(Boolean),
+ 'font-src': ["'self'"],
+ 'frame-src': ["'self'"],
+- 'img-src': ["'self'", 'data:'],
++ 'img-src': ["'self'", 'data:', 'https://*.example.com']
+```
+
+## Missing Icons
+
+Epic Stack uses SVG sprite icons for performance reasons. If you've received an
+error like this during local development:
+
+> X [ERROR] Could not resolve "#app/components/ui/icon.tsx"
+
+You need to be manually regenerate the icon with `npm run build:icons`.
+
+See
+[the icons decision document](https://github.com/epicweb-dev/epic-stack/blob/main/docs/decisions/020-icons.md)
+for more information about icons.
+
+## Hydration Mismatch
+
+If you see this error in the console:
+
+> Warning: Hydration failed because the initial UI does not match
+> server-rendered HTML.
+
+Read [this article](https://www.jacobparis.com/content/remix-hydration-errors)
+for more information about hydration errors and how to fix them.
+
+If the article does not apply to you, there are a few other things to note.
+
+Often people think the issue is caused by the `nonce` prop being empty on the
+client ([like this](https://github.com/epicweb-dev/epic-stack/discussions/768)).
+This is not going to be the problem
+[unless you're running Firefox during development mode](https://github.com/epicweb-dev/epic-stack/discussions/768#discussioncomment-10456308)
+(if that's the case then you can safely ignore it because that's a firefox bug
+that only affects development). The browser strips the `nonce` from the DOM
+before executing any JavaScript for security reasons and React handles this
+fine.
+
+Browser extensions are notorious for causing hydration errors in the Epic Stack.
+This is because we're using React to hydrate the entire document and many
+browser extensions add content to the `` which triggers a hydration error.
+
+In React 19, React will no longer have issues with differences in the ``
+so if you upgrade to React 19, you'll likely no longer see hydration errors for
+this reason.
diff --git a/examples/federation/epic-stack-remote/eslint.config.js b/examples/federation/epic-stack-remote/eslint.config.js
new file mode 100644
index 0000000..eede0cf
--- /dev/null
+++ b/examples/federation/epic-stack-remote/eslint.config.js
@@ -0,0 +1,14 @@
+import { default as defaultConfig } from '@epic-web/config/eslint'
+
+/** @type {import("eslint").Linter.Config} */
+export default [
+ ...defaultConfig,
+ // add custom config objects here:
+ {
+ files: ['**/tests/**/*.ts'],
+ rules: { 'react-hooks/rules-of-hooks': 'off' },
+ },
+ {
+ ignores: ['.react-router/*'],
+ },
+]
diff --git a/examples/federation/epic-stack-remote/fly.toml b/examples/federation/epic-stack-remote/fly.toml
new file mode 100644
index 0000000..7a24ed2
--- /dev/null
+++ b/examples/federation/epic-stack-remote/fly.toml
@@ -0,0 +1,61 @@
+app = "epic-stack-template"
+primary_region = "sjc"
+kill_signal = "SIGINT"
+kill_timeout = 5
+processes = [ ]
+swap_size_mb = 512
+
+[experimental]
+allowed_public_ports = [ ]
+auto_rollback = true
+
+[mounts]
+source = "data"
+destination = "/data"
+
+[build]
+dockerfile = "/other/Dockerfile"
+ignorefile = "/other/Dockerfile.dockerignore"
+
+[[services]]
+internal_port = 8080
+processes = [ "app" ]
+protocol = "tcp"
+script_checks = [ ]
+
+ [services.concurrency]
+ hard_limit = 100
+ soft_limit = 80
+ type = "requests"
+
+ [[services.ports]]
+ handlers = [ "http" ]
+ port = 80
+ force_https = true
+
+ [[services.ports]]
+ handlers = [ "tls", "http" ]
+ port = 443
+
+ [[services.tcp_checks]]
+ grace_period = "1s"
+ interval = "15s"
+ restart_limit = 0
+ timeout = "2s"
+
+ [[services.http_checks]]
+ interval = "10s"
+ grace_period = "5s"
+ method = "get"
+ path = "/resources/healthcheck"
+ protocol = "http"
+ timeout = "2s"
+ tls_skip_verify = false
+ headers = { }
+
+ [[services.http_checks]]
+ grace_period = "10s"
+ interval = "30s"
+ method = "GET"
+ timeout = "5s"
+ path = "/litefs/health"
diff --git a/examples/federation/epic-stack-remote/index.js b/examples/federation/epic-stack-remote/index.js
new file mode 100644
index 0000000..ac4f843
--- /dev/null
+++ b/examples/federation/epic-stack-remote/index.js
@@ -0,0 +1,31 @@
+import 'dotenv/config'
+import * as fs from 'node:fs'
+import sourceMapSupport from 'source-map-support'
+
+sourceMapSupport.install({
+ retrieveSourceMap: function (source) {
+ // get source file without the `file://` prefix or `?t=...` suffix
+ const match = source.match(/^file:\/\/(.*)\?t=[.\d]+$/)
+ if (match) {
+ return {
+ url: source,
+ map: fs.readFileSync(`${match[1]}.map`, 'utf8'),
+ }
+ }
+ return null
+ },
+})
+
+
+if (process.env.MOCKS === 'true') {
+ await import('./tests/mocks/index.ts')
+}
+
+if (process.env.NODE_ENV === 'production') {
+ let build = (await import('./build/server/static/js/app.js'))
+ build = build?.default || build;
+ build = build?.createApp || build
+ build();
+} else {
+ await import('./server/dev-build.js')
+}
diff --git a/examples/federation/epic-stack-remote/other/Dockerfile b/examples/federation/epic-stack-remote/other/Dockerfile
new file mode 100644
index 0000000..e1287b1
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/Dockerfile
@@ -0,0 +1,95 @@
+# This file is moved to the root directory before building the image
+
+# base node image
+FROM node:22-bookworm-slim as base
+
+# set for base and all layer that inherit from it
+ENV NODE_ENV production
+
+# Install openssl for Prisma
+RUN apt-get update && apt-get install -y fuse3 openssl sqlite3 ca-certificates
+
+# Install all node_modules, including dev dependencies
+FROM base as deps
+
+WORKDIR /myapp
+
+ADD package.json package-lock.json .npmrc ./
+RUN npm ci --include=dev
+
+# Setup production node_modules
+FROM base as production-deps
+
+WORKDIR /myapp
+
+COPY --from=deps /myapp/node_modules /myapp/node_modules
+ADD package.json package-lock.json .npmrc ./
+RUN npm prune --omit=dev
+
+# Build the app
+FROM base as build
+
+ARG COMMIT_SHA
+ENV COMMIT_SHA=$COMMIT_SHA
+
+# Use the following environment variables to configure Sentry
+# ENV SENTRY_ORG=
+# ENV SENTRY_PROJECT=
+
+
+WORKDIR /myapp
+
+COPY --from=deps /myapp/node_modules /myapp/node_modules
+
+ADD prisma .
+RUN npx prisma generate
+
+ADD . .
+
+# Mount the secret and set it as an environment variable and run the build
+RUN --mount=type=secret,id=SENTRY_AUTH_TOKEN \
+ export SENTRY_AUTH_TOKEN=$(cat /run/secrets/SENTRY_AUTH_TOKEN) && \
+ npm run build
+
+# Finally, build the production image with minimal footprint
+FROM base
+
+ENV FLY="true"
+ENV LITEFS_DIR="/litefs/data"
+ENV DATABASE_FILENAME="sqlite.db"
+ENV DATABASE_PATH="$LITEFS_DIR/$DATABASE_FILENAME"
+ENV DATABASE_URL="file:$DATABASE_PATH"
+ENV CACHE_DATABASE_FILENAME="cache.db"
+ENV CACHE_DATABASE_PATH="$LITEFS_DIR/$CACHE_DATABASE_FILENAME"
+ENV INTERNAL_PORT="8080"
+ENV PORT="8081"
+ENV NODE_ENV="production"
+# For WAL support: https://github.com/prisma/prisma-engines/issues/4675#issuecomment-1914383246
+ENV PRISMA_SCHEMA_DISABLE_ADVISORY_LOCK = "1"
+
+# add shortcut for connecting to database CLI
+RUN echo "#!/bin/sh\nset -x\nsqlite3 \$DATABASE_URL" > /usr/local/bin/database-cli && chmod +x /usr/local/bin/database-cli
+
+WORKDIR /myapp
+
+# Generate random value and save it to .env file which will be loaded by dotenv
+RUN INTERNAL_COMMAND_TOKEN=$(openssl rand -hex 32) && \
+ echo "INTERNAL_COMMAND_TOKEN=$INTERNAL_COMMAND_TOKEN" > .env
+
+COPY --from=production-deps /myapp/node_modules /myapp/node_modules
+COPY --from=build /myapp/node_modules/.prisma /myapp/node_modules/.prisma
+
+COPY --from=build /myapp/server-build /myapp/server-build
+COPY --from=build /myapp/build /myapp/build
+COPY --from=build /myapp/package.json /myapp/package.json
+COPY --from=build /myapp/prisma /myapp/prisma
+COPY --from=build /myapp/app/components/ui/icons /myapp/app/components/ui/icons
+
+# prepare for litefs
+COPY --from=flyio/litefs:0.5.11 /usr/local/bin/litefs /usr/local/bin/litefs
+ADD other/litefs.yml /etc/litefs.yml
+RUN mkdir -p /data ${LITEFS_DIR}
+
+ADD . .
+
+CMD ["litefs", "mount"]
diff --git a/examples/federation/epic-stack-remote/other/Dockerfile.dockerignore b/examples/federation/epic-stack-remote/other/Dockerfile.dockerignore
new file mode 100644
index 0000000..6e18765
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/Dockerfile.dockerignore
@@ -0,0 +1,9 @@
+# This file is moved to the root directory before building the image
+
+/node_modules
+*.log
+.DS_Store
+.env
+/.cache
+/public/build
+/build
diff --git a/examples/federation/epic-stack-remote/other/README.md b/examples/federation/epic-stack-remote/other/README.md
new file mode 100644
index 0000000..a934f46
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/README.md
@@ -0,0 +1,9 @@
+# Other
+
+The "other" directory is where we put stuff that doesn't really have a place,
+but we don't want in the root of the project. In fact, we want to move as much
+stuff here from the root as possible. The only things that should stay in the
+root directory are those things that have to stay in the root for most editor
+and other tool integrations (like most configuration files sadly). Maybe one day
+we can convince tools to adopt a new `.config` directory in the future. Until
+then, we've got this `./other` directory to keep things cleaner.
diff --git a/examples/federation/epic-stack-remote/other/build-icons.ts b/examples/federation/epic-stack-remote/other/build-icons.ts
new file mode 100644
index 0000000..ccaaf1a
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/build-icons.ts
@@ -0,0 +1,153 @@
+import * as path from 'node:path'
+import { $ } from 'execa'
+import fsExtra from 'fs-extra'
+import { glob } from 'glob'
+import { parse } from 'node-html-parser'
+
+const cwd = process.cwd()
+const inputDir = path.join(cwd, 'other', 'svg-icons')
+const inputDirRelative = path.relative(cwd, inputDir)
+const outputDir = path.join(cwd, 'app', 'components', 'ui', 'icons')
+await fsExtra.ensureDir(outputDir)
+
+const files = glob
+ .sync('**/*.svg', {
+ cwd: inputDir,
+ })
+ .sort((a, b) => a.localeCompare(b))
+
+const shouldVerboseLog = process.argv.includes('--log=verbose')
+const logVerbose = shouldVerboseLog ? console.log : () => {}
+
+if (files.length === 0) {
+ console.log(`No SVG files found in ${inputDirRelative}`)
+} else {
+ await generateIconFiles()
+}
+
+async function generateIconFiles() {
+ const spriteFilepath = path.join(outputDir, 'sprite.svg')
+ const typeOutputFilepath = path.join(outputDir, 'name.d.ts')
+ const currentSprite = await fsExtra
+ .readFile(spriteFilepath, 'utf8')
+ .catch(() => '')
+ const currentTypes = await fsExtra
+ .readFile(typeOutputFilepath, 'utf8')
+ .catch(() => '')
+
+ const iconNames = files.map((file) => iconName(file))
+
+ const spriteUpToDate = iconNames.every((name) =>
+ currentSprite.includes(`id=${name}`),
+ )
+ const typesUpToDate = iconNames.every((name) =>
+ currentTypes.includes(`"${name}"`),
+ )
+
+ if (spriteUpToDate && typesUpToDate) {
+ logVerbose(`Icons are up to date`)
+ return
+ }
+
+ logVerbose(`Generating sprite for ${inputDirRelative}`)
+
+ const spriteChanged = await generateSvgSprite({
+ files,
+ inputDir,
+ outputPath: spriteFilepath,
+ })
+
+ for (const file of files) {
+ logVerbose('β
', file)
+ }
+ logVerbose(`Saved to ${path.relative(cwd, spriteFilepath)}`)
+
+ const stringifiedIconNames = iconNames.map((name) => JSON.stringify(name))
+
+ const typeOutputContent = `// This file is generated by npm run build:icons
+
+export type IconName =
+\t| ${stringifiedIconNames.join('\n\t| ')};
+`
+ const typesChanged = await writeIfChanged(
+ typeOutputFilepath,
+ typeOutputContent,
+ )
+
+ logVerbose(`Manifest saved to ${path.relative(cwd, typeOutputFilepath)}`)
+
+ const readmeChanged = await writeIfChanged(
+ path.join(outputDir, 'README.md'),
+ `# Icons
+
+This directory contains SVG icons that are used by the app.
+
+Everything in this directory is generated by \`npm run build:icons\`.
+`,
+ )
+
+ if (spriteChanged || typesChanged || readmeChanged) {
+ console.log(`Generated ${files.length} icons`)
+ }
+}
+
+function iconName(file: string) {
+ return file.replace(/\.svg$/, '')
+}
+
+/**
+ * Creates a single SVG file that contains all the icons
+ */
+async function generateSvgSprite({
+ files,
+ inputDir,
+ outputPath,
+}: {
+ files: string[]
+ inputDir: string
+ outputPath: string
+}) {
+ // Each SVG becomes a symbol and we wrap them all in a single SVG
+ const symbols = await Promise.all(
+ files.map(async (file) => {
+ const input = await fsExtra.readFile(path.join(inputDir, file), 'utf8')
+ const root = parse(input)
+
+ const svg = root.querySelector('svg')
+ if (!svg) throw new Error('No SVG element found')
+
+ svg.tagName = 'symbol'
+ svg.setAttribute('id', iconName(file))
+ svg.removeAttribute('xmlns')
+ svg.removeAttribute('xmlns:xlink')
+ svg.removeAttribute('version')
+ svg.removeAttribute('width')
+ svg.removeAttribute('height')
+
+ return svg.toString().trim()
+ }),
+ )
+
+ const output = [
+ ``,
+ ``,
+ ``,
+ ``, // for semantics: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs
+ ...symbols,
+ ` `,
+ ` `,
+ '', // trailing newline
+ ].join('\n')
+
+ return writeIfChanged(outputPath, output)
+}
+
+async function writeIfChanged(filepath: string, newContent: string) {
+ const currentContent = await fsExtra
+ .readFile(filepath, 'utf8')
+ .catch(() => '')
+ if (currentContent === newContent) return false
+ await fsExtra.writeFile(filepath, newContent, 'utf8')
+ await $`prettier --write ${filepath} --ignore-unknown`
+ return true
+}
diff --git a/examples/federation/epic-stack-remote/other/build-server.ts b/examples/federation/epic-stack-remote/other/build-server.ts
new file mode 100644
index 0000000..896b0f6
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/build-server.ts
@@ -0,0 +1,50 @@
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import esbuild from 'esbuild'
+import fsExtra from 'fs-extra'
+import { globSync } from 'glob'
+
+const pkg = fsExtra.readJsonSync(path.join(process.cwd(), 'package.json'))
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const here = (...s: Array) => path.join(__dirname, ...s)
+const globsafe = (s: string) => s.replace(/\\/g, '/')
+
+const allFiles = globSync(globsafe(here('../server/**/*.*')), {
+ ignore: [
+ 'server/dev-server.js', // for development only
+ '**/tsconfig.json',
+ '**/eslint*',
+ '**/__tests__/**',
+ ],
+})
+
+const entries = []
+for (const file of allFiles) {
+ if (/\.(ts|js|tsx|jsx)$/.test(file)) {
+ entries.push(file)
+ } else {
+ const dest = file.replace(here('../server'), here('../server-build'))
+ fsExtra.ensureDirSync(path.parse(dest).dir)
+ fsExtra.copySync(file, dest)
+ console.log(`copied: ${file.replace(`${here('../server')}/`, '')}`)
+ }
+}
+
+console.log()
+console.log('building...')
+
+esbuild
+ .build({
+ entryPoints: entries,
+ outdir: here('../server-build'),
+ target: [`node${pkg.engines.node}`],
+ platform: 'node',
+ sourcemap: true,
+ format: 'esm',
+ logLevel: 'info',
+ })
+ .catch((error: unknown) => {
+ console.error(error)
+ process.exit(1)
+ })
diff --git a/examples/federation/epic-stack-remote/other/litefs.yml b/examples/federation/epic-stack-remote/other/litefs.yml
new file mode 100644
index 0000000..8ccc5bb
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/litefs.yml
@@ -0,0 +1,43 @@
+# Documented example: https://github.com/superfly/litefs/blob/dec5a7353292068b830001bd2df4830e646f6a2f/cmd/litefs/etc/litefs.yml
+fuse:
+ # Required. This is the mount directory that applications will
+ # use to access their SQLite databases.
+ dir: '${LITEFS_DIR}'
+
+data:
+ # Path to internal data storage.
+ dir: '/data/litefs'
+
+proxy:
+ # matches the internal_port in fly.toml
+ addr: ':${INTERNAL_PORT}'
+ target: 'localhost:${PORT}'
+ db: '${DATABASE_FILENAME}'
+
+# The lease section specifies how the cluster will be managed. We're using the
+# "consul" lease type so that our application can dynamically change the primary.
+#
+# These environment variables will be available in your Fly.io application.
+lease:
+ type: 'consul'
+ candidate: ${FLY_REGION == PRIMARY_REGION}
+ promote: true
+ advertise-url: 'http://${HOSTNAME}.vm.${FLY_APP_NAME}.internal:20202'
+
+ consul:
+ url: '${FLY_CONSUL_URL}'
+ key: 'epic-stack-litefs/${FLY_APP_NAME}'
+
+exec:
+ - cmd: npx prisma migrate deploy
+ if-candidate: true
+
+ # Set the journal mode for the database to WAL. This reduces concurrency deadlock issues
+ - cmd: sqlite3 $DATABASE_PATH "PRAGMA journal_mode = WAL;"
+ if-candidate: true
+
+ # Set the journal mode for the cache to WAL. This reduces concurrency deadlock issues
+ - cmd: sqlite3 $CACHE_DATABASE_PATH "PRAGMA journal_mode = WAL;"
+ if-candidate: true
+
+ - cmd: npm start
diff --git a/examples/federation/epic-stack-remote/other/sly/sly.json b/examples/federation/epic-stack-remote/other/sly/sly.json
new file mode 100644
index 0000000..4f109e7
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/sly/sly.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://sly-cli.fly.dev/registry/config.json",
+ "libraries": [
+ {
+ "name": "@radix-ui/icons",
+ "directory": "./other/svg-icons",
+ "postinstall": ["npm", "run", "build:icons"],
+ "transformers": ["transform-icon.ts"]
+ }
+ ]
+}
diff --git a/examples/federation/epic-stack-remote/other/sly/transform-icon.ts b/examples/federation/epic-stack-remote/other/sly/transform-icon.ts
new file mode 100644
index 0000000..8100773
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/sly/transform-icon.ts
@@ -0,0 +1,19 @@
+import { type Meta } from '@sly-cli/sly'
+
+/**
+ * @type {import('@sly-cli/sly/dist').Transformer}
+ */
+export default function transformIcon(input: string, meta: Meta) {
+ input = prependLicenseInfo(input, meta)
+
+ return input
+}
+
+function prependLicenseInfo(input: string, meta: Meta): string {
+ return [
+ ``,
+ ``,
+ ``,
+ input,
+ ].join('\n')
+}
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/README.md b/examples/federation/epic-stack-remote/other/svg-icons/README.md
new file mode 100644
index 0000000..a9be9dd
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/README.md
@@ -0,0 +1,11 @@
+# Icons
+
+These icons were downloaded from https://icons.radix-ui.com/ which is licensed
+under MIT: https://github.com/radix-ui/icons/blob/master/LICENSE
+
+It's important that you only add icons to this directory that the application
+actually needs as there's no "tree-shaking" for sprites. If you wish to manually
+split up your SVG sprite into multiple files, you'll need to update the
+`build-icons.ts` script to do that.
+
+Run `npm run build:icons` to update the sprite.
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/arrow-left.svg b/examples/federation/epic-stack-remote/other/svg-icons/arrow-left.svg
new file mode 100644
index 0000000..d59c7a6
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/arrow-left.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/arrow-right.svg b/examples/federation/epic-stack-remote/other/svg-icons/arrow-right.svg
new file mode 100644
index 0000000..2c3424a
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/arrow-right.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/avatar.svg b/examples/federation/epic-stack-remote/other/svg-icons/avatar.svg
new file mode 100644
index 0000000..14acc3d
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/avatar.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/camera.svg b/examples/federation/epic-stack-remote/other/svg-icons/camera.svg
new file mode 100644
index 0000000..c5f1d35
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/camera.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/check.svg b/examples/federation/epic-stack-remote/other/svg-icons/check.svg
new file mode 100644
index 0000000..e057a8b
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/check.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/clock.svg b/examples/federation/epic-stack-remote/other/svg-icons/clock.svg
new file mode 100644
index 0000000..630c19b
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/clock.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/cross-1.svg b/examples/federation/epic-stack-remote/other/svg-icons/cross-1.svg
new file mode 100644
index 0000000..b94ccfe
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/cross-1.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/dots-horizontal.svg b/examples/federation/epic-stack-remote/other/svg-icons/dots-horizontal.svg
new file mode 100644
index 0000000..fa7758f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/dots-horizontal.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/download.svg b/examples/federation/epic-stack-remote/other/svg-icons/download.svg
new file mode 100644
index 0000000..c5a617b
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/download.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/envelope-closed.svg b/examples/federation/epic-stack-remote/other/svg-icons/envelope-closed.svg
new file mode 100644
index 0000000..6a9d3f0
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/envelope-closed.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/exit.svg b/examples/federation/epic-stack-remote/other/svg-icons/exit.svg
new file mode 100644
index 0000000..292f94d
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/exit.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/file-text.svg b/examples/federation/epic-stack-remote/other/svg-icons/file-text.svg
new file mode 100644
index 0000000..5fe1b7e
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/file-text.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/github-logo.svg b/examples/federation/epic-stack-remote/other/svg-icons/github-logo.svg
new file mode 100644
index 0000000..7fbb0a9
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/github-logo.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/laptop.svg b/examples/federation/epic-stack-remote/other/svg-icons/laptop.svg
new file mode 100644
index 0000000..0297959
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/laptop.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/link-2.svg b/examples/federation/epic-stack-remote/other/svg-icons/link-2.svg
new file mode 100644
index 0000000..f36d6d9
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/link-2.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/lock-closed.svg b/examples/federation/epic-stack-remote/other/svg-icons/lock-closed.svg
new file mode 100644
index 0000000..677bdc7
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/lock-closed.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/lock-open-1.svg b/examples/federation/epic-stack-remote/other/svg-icons/lock-open-1.svg
new file mode 100644
index 0000000..789f9d4
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/lock-open-1.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/magnifying-glass.svg b/examples/federation/epic-stack-remote/other/svg-icons/magnifying-glass.svg
new file mode 100644
index 0000000..670396b
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/magnifying-glass.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/moon.svg b/examples/federation/epic-stack-remote/other/svg-icons/moon.svg
new file mode 100644
index 0000000..5df2660
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/moon.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/pencil-1.svg b/examples/federation/epic-stack-remote/other/svg-icons/pencil-1.svg
new file mode 100644
index 0000000..2695e8c
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/pencil-1.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/pencil-2.svg b/examples/federation/epic-stack-remote/other/svg-icons/pencil-2.svg
new file mode 100644
index 0000000..a249a09
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/pencil-2.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/plus.svg b/examples/federation/epic-stack-remote/other/svg-icons/plus.svg
new file mode 100644
index 0000000..5009fac
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/plus.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/question-mark-circled.svg b/examples/federation/epic-stack-remote/other/svg-icons/question-mark-circled.svg
new file mode 100644
index 0000000..c99757d
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/question-mark-circled.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/reset.svg b/examples/federation/epic-stack-remote/other/svg-icons/reset.svg
new file mode 100644
index 0000000..41c2e88
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/reset.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/sun.svg b/examples/federation/epic-stack-remote/other/svg-icons/sun.svg
new file mode 100644
index 0000000..b3891c9
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/sun.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/trash.svg b/examples/federation/epic-stack-remote/other/svg-icons/trash.svg
new file mode 100644
index 0000000..472954b
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/trash.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/other/svg-icons/update.svg b/examples/federation/epic-stack-remote/other/svg-icons/update.svg
new file mode 100644
index 0000000..c95c60d
--- /dev/null
+++ b/examples/federation/epic-stack-remote/other/svg-icons/update.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/examples/federation/epic-stack-remote/package-lock.json b/examples/federation/epic-stack-remote/package-lock.json
new file mode 100644
index 0000000..68d14d1
--- /dev/null
+++ b/examples/federation/epic-stack-remote/package-lock.json
@@ -0,0 +1,17276 @@
+{
+ "name": "epic-stack",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "epic-stack",
+ "license": "MIT",
+ "dependencies": {
+ "@conform-to/react": "^1.2.2",
+ "@conform-to/zod": "^1.2.2",
+ "@epic-web/cachified": "^5.2.0",
+ "@epic-web/client-hints": "^1.3.5",
+ "@epic-web/invariant": "^1.0.0",
+ "@epic-web/remember": "^1.1.0",
+ "@epic-web/totp": "^2.1.1",
+ "@mjackson/form-data-parser": "^0.7.0",
+ "@nasa-gcn/remix-seo": "^2.0.1",
+ "@oslojs/crypto": "^1.0.1",
+ "@oslojs/encoding": "^1.1.0",
+ "@paralleldrive/cuid2": "^2.2.2",
+ "@prisma/client": "^6.3.1",
+ "@prisma/instrumentation": "^6.3.1",
+ "@radix-ui/react-checkbox": "^1.1.3",
+ "@radix-ui/react-dropdown-menu": "^2.1.5",
+ "@radix-ui/react-label": "^2.1.1",
+ "@radix-ui/react-slot": "^1.1.1",
+ "@radix-ui/react-toast": "^1.2.5",
+ "@radix-ui/react-tooltip": "^1.1.7",
+ "@react-email/components": "0.0.32",
+ "@react-router/express": "^7.1.5",
+ "@react-router/node": "^7.1.5",
+ "@react-router/remix-routes-option-adapter": "^7.1.5",
+ "@remix-run/server-runtime": "^2.15.3",
+ "@sentry/node": "^8.54.0",
+ "@sentry/profiling-node": "^8.54.0",
+ "@sentry/react": "^8.54.0",
+ "@tusbar/cache-control": "1.0.2",
+ "address": "^2.0.3",
+ "bcryptjs": "^2.4.3",
+ "better-sqlite3": "^11.8.1",
+ "chalk": "^5.4.1",
+ "class-variance-authority": "^0.7.1",
+ "close-with-grace": "^2.2.0",
+ "clsx": "^2.1.1",
+ "compression": "^1.7.5",
+ "cookie": "^1.0.2",
+ "cross-env": "^7.0.3",
+ "date-fns": "^4.1.0",
+ "dotenv": "^16.4.7",
+ "execa": "^9.5.2",
+ "express": "^4.21.2",
+ "express-rate-limit": "^7.5.0",
+ "get-port": "^7.1.0",
+ "glob": "^11.0.1",
+ "helmet": "^8.0.0",
+ "input-otp": "^1.4.2",
+ "intl-parse-accept-language": "^1.0.0",
+ "isbot": "^5.1.22",
+ "litefs-js": "^1.1.2",
+ "lru-cache": "^11.0.2",
+ "morgan": "^1.10.0",
+ "prisma": "^6.3.1",
+ "qrcode": "^1.5.4",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-router": "7.1.5",
+ "remix-auth": "^3.7.0",
+ "remix-auth-github": "^1.7.0",
+ "remix-utils": "^8.1.0",
+ "set-cookie-parser": "^2.7.1",
+ "sonner": "^1.7.4",
+ "source-map-support": "^0.5.21",
+ "spin-delay": "^2.0.1",
+ "tailwind-merge": "^2.6.0",
+ "tailwindcss": "^3.4.17",
+ "tailwindcss-animate": "^1.0.7",
+ "tailwindcss-radix": "^3.0.5",
+ "vite-env-only": "^3.0.3",
+ "zod": "^3.24.1"
+ },
+ "devDependencies": {
+ "@epic-web/config": "^1.16.5",
+ "@faker-js/faker": "^9.4.0",
+ "@playwright/test": "^1.50.1",
+ "@react-router/dev": "^7.1.5",
+ "@sentry/vite-plugin": "^3.1.2",
+ "@sly-cli/sly": "^1.14.0",
+ "@testing-library/dom": "^10.4.0",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.2.0",
+ "@testing-library/user-event": "^14.6.1",
+ "@total-typescript/ts-reset": "^0.6.1",
+ "@types/bcryptjs": "^2.4.6",
+ "@types/better-sqlite3": "^7.6.12",
+ "@types/compression": "^1.7.5",
+ "@types/eslint": "^9.6.1",
+ "@types/express": "^4.17.21",
+ "@types/fs-extra": "^11.0.4",
+ "@types/glob": "^8.1.0",
+ "@types/morgan": "^1.9.9",
+ "@types/node": "^22.13.1",
+ "@types/qrcode": "^1.5.5",
+ "@types/react": "^19.0.8",
+ "@types/react-dom": "^19.0.3",
+ "@types/set-cookie-parser": "^2.4.10",
+ "@types/source-map-support": "^0.5.10",
+ "@vitejs/plugin-react": "^4.3.4",
+ "@vitest/coverage-v8": "^3.0.5",
+ "autoprefixer": "^10.4.20",
+ "enforce-unique": "^1.3.0",
+ "esbuild": "^0.24.2",
+ "eslint": "^9.19.0",
+ "fs-extra": "^11.3.0",
+ "jsdom": "^25.0.1",
+ "msw": "^2.7.0",
+ "node-html-parser": "^7.0.1",
+ "npm-run-all": "^4.1.5",
+ "prettier": "^3.4.2",
+ "prettier-plugin-sql": "^0.18.1",
+ "prettier-plugin-tailwindcss": "^0.6.11",
+ "remix-flat-routes": "^0.8.4",
+ "tsx": "^4.19.2",
+ "typescript": "^5.7.3",
+ "vite": "^6.0.11",
+ "vitest": "^3.0.5"
+ },
+ "engines": {
+ "node": "22"
+ }
+ },
+ "node_modules/@adobe/css-tools": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.1.tgz",
+ "integrity": "sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.26.2",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
+ "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.25.9",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.26.3",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz",
+ "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
+ "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.26.0",
+ "@babel/generator": "^7.26.0",
+ "@babel/helper-compilation-targets": "^7.25.9",
+ "@babel/helper-module-transforms": "^7.26.0",
+ "@babel/helpers": "^7.26.0",
+ "@babel/parser": "^7.26.0",
+ "@babel/template": "^7.25.9",
+ "@babel/traverse": "^7.25.9",
+ "@babel/types": "^7.26.0",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.26.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz",
+ "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.26.3",
+ "@babel/types": "^7.26.3",
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz",
+ "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz",
+ "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.25.9",
+ "@babel/helper-validator-option": "^7.25.9",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz",
+ "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.25.9",
+ "@babel/helper-member-expression-to-functions": "^7.25.9",
+ "@babel/helper-optimise-call-expression": "^7.25.9",
+ "@babel/helper-replace-supers": "^7.25.9",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9",
+ "@babel/traverse": "^7.25.9",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz",
+ "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.25.9",
+ "@babel/types": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
+ "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.25.9",
+ "@babel/types": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
+ "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.25.9",
+ "@babel/traverse": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz",
+ "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz",
+ "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz",
+ "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-member-expression-to-functions": "^7.25.9",
+ "@babel/helper-optimise-call-expression": "^7.25.9",
+ "@babel/traverse": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz",
+ "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.25.9",
+ "@babel/types": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
+ "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+ "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
+ "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
+ "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.25.9",
+ "@babel/types": "^7.26.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.26.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
+ "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.26.3"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-decorators": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.25.9.tgz",
+ "integrity": "sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz",
+ "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz",
+ "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "7.26.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz",
+ "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.26.0",
+ "@babel/helper-plugin-utils": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz",
+ "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz",
+ "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typescript": {
+ "version": "7.26.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.3.tgz",
+ "integrity": "sha512-6+5hpdr6mETwSKjmJUdYw0EIkATiQhnELWlE3kJFBwSg/BGIVwVaVbX+gOXBCdc7Ln1RXZxyWGecIXhUfnl7oA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.25.9",
+ "@babel/helper-create-class-features-plugin": "^7.25.9",
+ "@babel/helper-plugin-utils": "^7.25.9",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9",
+ "@babel/plugin-syntax-typescript": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/preset-typescript": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz",
+ "integrity": "sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.25.9",
+ "@babel/helper-validator-option": "^7.25.9",
+ "@babel/plugin-syntax-jsx": "^7.25.9",
+ "@babel/plugin-transform-modules-commonjs": "^7.25.9",
+ "@babel/plugin-transform-typescript": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
+ "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
+ "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.25.9",
+ "@babel/parser": "^7.25.9",
+ "@babel/types": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.26.4",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz",
+ "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.26.2",
+ "@babel/generator": "^7.26.3",
+ "@babel/parser": "^7.26.3",
+ "@babel/template": "^7.25.9",
+ "@babel/types": "^7.26.3",
+ "debug": "^4.3.1",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.26.3",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
+ "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@bundled-es-modules/cookie": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz",
+ "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cookie": "^0.7.2"
+ }
+ },
+ "node_modules/@bundled-es-modules/cookie/node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@bundled-es-modules/statuses": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz",
+ "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "statuses": "^2.0.1"
+ }
+ },
+ "node_modules/@bundled-es-modules/tough-cookie": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz",
+ "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@types/tough-cookie": "^4.0.5",
+ "tough-cookie": "^4.1.4"
+ }
+ },
+ "node_modules/@bundled-es-modules/tough-cookie/node_modules/tough-cookie": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
+ "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.2.0",
+ "url-parse": "^1.5.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@bundled-es-modules/tough-cookie/node_modules/universalify": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
+ "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/@conform-to/dom": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@conform-to/dom/-/dom-1.2.2.tgz",
+ "integrity": "sha512-f05EClpNP31o6lX4LYmmLqgsiTOHdGfY7z2XXK6U6rRp+EtxrkUBdrFlIGsfkf7e9AFO19h3/Cb/cXHVd1k1FA==",
+ "license": "MIT"
+ },
+ "node_modules/@conform-to/react": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@conform-to/react/-/react-1.2.2.tgz",
+ "integrity": "sha512-1JBECb3NKi5/IlexaYLgnAxGJ55MRuO2sEQ10cJfUK2bfltNbTIQnYUDG6pU886A4lda/q6UH/adPsjiB/4Gkg==",
+ "license": "MIT",
+ "dependencies": {
+ "@conform-to/dom": "1.2.2"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@conform-to/zod": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@conform-to/zod/-/zod-1.2.2.tgz",
+ "integrity": "sha512-mNCzh0XsF2vhCtD8bfHYMYayEJ9dP6/KsGjmq8DFcO1ykDTNQZwfi1MIm4evGYVempSS3poYr4xZjd7cXEbtaw==",
+ "license": "MIT",
+ "dependencies": {
+ "@conform-to/dom": "1.2.2"
+ },
+ "peerDependencies": {
+ "zod": "^3.21.0"
+ }
+ },
+ "node_modules/@epic-web/cachified": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@epic-web/cachified/-/cachified-5.2.0.tgz",
+ "integrity": "sha512-E/2LdIhS/wcn3ykV+u5xbTFahKtzReO0k4/cVtE7KBYiLCgR6bPRmlDUuyfWhbZX3zOmK+6OL7qdHkvgCWwSiA==",
+ "license": "MIT"
+ },
+ "node_modules/@epic-web/client-hints": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@epic-web/client-hints/-/client-hints-1.3.5.tgz",
+ "integrity": "sha512-tFIDxdU5NzN5Ak4gcDOPKkj6aF/qNMC0G+K58CTBZIx7CMSjCrxqhuiEbZBKGDAGJcsQLF5uKKlgs6mgqWmB7Q==",
+ "license": "MIT"
+ },
+ "node_modules/@epic-web/config": {
+ "version": "1.16.5",
+ "resolved": "https://registry.npmjs.org/@epic-web/config/-/config-1.16.5.tgz",
+ "integrity": "sha512-FmIyJV7LJRMAsLrmylRPCXpNERNmBQ9QqoXMq2TsYyyZoWwZchHt8MKhPPpGq7p7DQpbk1rmbA2Wi5h3htcJAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@total-typescript/ts-reset": "^0.6.1",
+ "@vitest/eslint-plugin": "^1.1.14",
+ "eslint-plugin-import-x": "^4.5.0",
+ "eslint-plugin-jest-dom": "^5.5.0",
+ "eslint-plugin-react": "^7.37.2",
+ "eslint-plugin-react-hooks": "^5.1.0",
+ "eslint-plugin-testing-library": "^7.1.0",
+ "globals": "^15.13.0",
+ "prettier-plugin-tailwindcss": "^0.6.9",
+ "tslib": "^2.8.1",
+ "typescript-eslint": "^8.17.0"
+ }
+ },
+ "node_modules/@epic-web/invariant": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
+ "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
+ "license": "MIT"
+ },
+ "node_modules/@epic-web/remember": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@epic-web/remember/-/remember-1.1.0.tgz",
+ "integrity": "sha512-FIhO7PFUVEbcnrJOtom8gb4GXog4Z44n4Jxwmw2nkKt4mx8I/q/d0O4tMabjYndM1QX2oXvRYzpZxtP61s2P5A==",
+ "license": "MIT"
+ },
+ "node_modules/@epic-web/totp": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@epic-web/totp/-/totp-2.1.1.tgz",
+ "integrity": "sha512-y5zmjcFm4sQ2Y89ASW7Znl9jTmtK1AZOuQ7daVRdPxnVCmaKomnlP7poJwoN1B0AeXzt+ILVQ6SAwhg+nGd2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "base32-decode": "^1.0.0",
+ "base32-encode": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
+ "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
+ "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
+ "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
+ "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
+ "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
+ "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
+ "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
+ "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
+ "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
+ "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
+ "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
+ "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
+ "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
+ "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
+ "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
+ "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
+ "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
+ "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
+ "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
+ "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
+ "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
+ "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
+ "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
+ "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
+ "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
+ "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz",
+ "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.5",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz",
+ "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
+ "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.19.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz",
+ "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz",
+ "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz",
+ "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.10.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@faker-js/faker": {
+ "version": "9.4.0",
+ "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.4.0.tgz",
+ "integrity": "sha512-85+k0AxaZSTowL0gXp8zYWDIrWclTbRPg/pm/V0dSFZ6W6D4lhcG3uuZl4zLsEKfEvs69xDbLN2cHQudwp95JA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fakerjs"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=9.0.0"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.6.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz",
+ "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.8"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.6.12",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz",
+ "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.6.0",
+ "@floating-ui/utils": "^0.2.8"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
+ "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
+ "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
+ "license": "MIT"
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.6",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+ "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+ "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
+ "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@inquirer/confirm": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.1.tgz",
+ "integrity": "sha512-vVLSbGci+IKQvDOtzpPTCOiEJCNidHcAq9JYVoWTW0svb5FiwSLotkM+JXNXejfjnzVYV9n0DTBythl9+XgTxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.1.2",
+ "@inquirer/type": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/core": {
+ "version": "10.1.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.2.tgz",
+ "integrity": "sha512-bHd96F3ezHg1mf/J0Rb4CV8ndCN0v28kUlrHqP7+ECm1C/A+paB7Xh2lbMk6x+kweQC+rZOxM/YeKikzxco8bQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/figures": "^1.0.9",
+ "@inquirer/type": "^3.0.2",
+ "ansi-escapes": "^4.3.2",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^2.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/core/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@inquirer/core/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/@inquirer/core/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@inquirer/core/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@inquirer/core/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@inquirer/core/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@inquirer/core/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@inquirer/figures": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.9.tgz",
+ "integrity": "sha512-BXvGj0ehzrngHTPTDqUoDT3NXL8U0RxUk2zJm2A66RhCEIWdtU1v6GuUqNAgArW4PQ9CinqIWyHdQgdwOj06zQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/type": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.2.tgz",
+ "integrity": "sha512-ZhQ4TvhwHZF+lGhQ2O/rsjo80XoZR5/5qhOY3t6FJuX5XBg5Be8YzYTvaUGJnc12AUGI2nr4QSUE4PhKSigx7g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+ "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@mjackson/form-data-parser": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@mjackson/form-data-parser/-/form-data-parser-0.7.0.tgz",
+ "integrity": "sha512-Y8O5+nsTv4K9Q8ziyuoru8JqYTLsP1PRi7xiFDAx4vrXEvO16NtOO7RmvXIJ2ZB59gP/wV3X1OvqdpoeRgsruA==",
+ "license": "MIT",
+ "dependencies": {
+ "@mjackson/multipart-parser": "^0.8.0"
+ }
+ },
+ "node_modules/@mjackson/headers": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@mjackson/headers/-/headers-0.10.0.tgz",
+ "integrity": "sha512-U1Eu1gF979k7ZoIBsJyD+T5l9MjtPONsZfoXfktsQHPJD0s7SokBGx+tLKDLsOY+gzVYAWS0yRFDNY8cgbQzWQ==",
+ "license": "MIT"
+ },
+ "node_modules/@mjackson/multipart-parser": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/@mjackson/multipart-parser/-/multipart-parser-0.8.2.tgz",
+ "integrity": "sha512-KltttyypazaJ9kD1GpiOTEop9/YA5aZPwKfpbmuMYoYSyJhQc+0pqaQcZSHUJVdJBvIWgx7TTQSDJdnNqP5dxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@mjackson/headers": "^0.10.0"
+ }
+ },
+ "node_modules/@mjackson/node-fetch-server": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@mjackson/node-fetch-server/-/node-fetch-server-0.2.0.tgz",
+ "integrity": "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==",
+ "license": "MIT"
+ },
+ "node_modules/@mswjs/interceptors": {
+ "version": "0.37.3",
+ "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.3.tgz",
+ "integrity": "sha512-USvgCL/uOGFtVa6SVyRrC8kIAedzRohxIXN5LISlg5C5vLZCn7dgMFVSNhSF9cuBEFrm/O2spDWEZeMnw4ZXYg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@open-draft/deferred-promise": "^2.2.0",
+ "@open-draft/logger": "^0.3.0",
+ "@open-draft/until": "^2.0.0",
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.3",
+ "strict-event-emitter": "^0.5.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nasa-gcn/remix-seo": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@nasa-gcn/remix-seo/-/remix-seo-2.0.1.tgz",
+ "integrity": "sha512-g9biDdYfsdFBnOU7lM+7vPGEXSEMRnWmfVLDQ98pT0PnTT/O3pFuA+s3DA0Mj9IwnAq9IcLs2Wee/aL6fvEA+A==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.21"
+ },
+ "peerDependencies": {
+ "@remix-run/react": "^1.0.0 || ^2.0.0",
+ "@remix-run/server-runtime": "^1.0.0 || ^2.0.0"
+ }
+ },
+ "node_modules/@noble/hashes": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz",
+ "integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@npmcli/git": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz",
+ "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/promise-spawn": "^6.0.0",
+ "lru-cache": "^7.4.4",
+ "npm-pick-manifest": "^8.0.0",
+ "proc-log": "^3.0.0",
+ "promise-inflight": "^1.0.1",
+ "promise-retry": "^2.0.1",
+ "semver": "^7.3.5",
+ "which": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/git/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@npmcli/package-json": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-4.0.1.tgz",
+ "integrity": "sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^4.1.0",
+ "glob": "^10.2.2",
+ "hosted-git-info": "^6.1.1",
+ "json-parse-even-better-errors": "^3.0.0",
+ "normalize-package-data": "^5.0.0",
+ "proc-log": "^3.0.0",
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/package-json/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@npmcli/package-json/node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@npmcli/package-json/node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/@npmcli/package-json/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@npmcli/package-json/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@npmcli/package-json/node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@npmcli/promise-spawn": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz",
+ "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "which": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@open-draft/deferred-promise": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
+ "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@open-draft/logger": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
+ "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.0"
+ }
+ },
+ "node_modules/@open-draft/until": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
+ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@opentelemetry/api": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
+ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/api-logs": {
+ "version": "0.57.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.1.tgz",
+ "integrity": "sha512-I4PHczeujhQAQv6ZBzqHYEUiggZL4IdSMixtVD3EYqbdrjujE7kRfI5QohjlPoJm8BvenoW5YaTMWRrbpot6tg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/context-async-hooks": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz",
+ "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/core": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz",
+ "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation": {
+ "version": "0.57.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.1.tgz",
+ "integrity": "sha512-SgHEKXoVxOjc20ZYusPG3Fh+RLIZTSa4x8QtD3NfgAUDyqdFFS9W1F2ZVbZkqDCdyMcQG02Ok4duUGLHJXHgbA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.57.1",
+ "@types/shimmer": "^1.2.0",
+ "import-in-the-middle": "^1.8.1",
+ "require-in-the-middle": "^7.1.1",
+ "semver": "^7.5.2",
+ "shimmer": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-amqplib": {
+ "version": "0.46.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.46.0.tgz",
+ "integrity": "sha512-04VHHV1KIN/c1wLWwzmLI02d/welgscBJ4BuDqrHaxd+ZIdlVXK9UYQsYf3JwSeF52z/4YoSzr8bfdVBSWoMAg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-connect": {
+ "version": "0.43.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.43.0.tgz",
+ "integrity": "sha512-Q57JGpH6T4dkYHo9tKXONgLtxzsh1ZEW5M9A/OwKrZFyEpLqWgjhcZ3hIuVvDlhb426iDF1f9FPToV/mi5rpeA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0",
+ "@types/connect": "3.4.36"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-dataloader": {
+ "version": "0.16.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.16.0.tgz",
+ "integrity": "sha512-88+qCHZC02up8PwKHk0UQKLLqGGURzS3hFQBZC7PnGwReuoKjHXS1o29H58S+QkXJpkTr2GACbx8j6mUoGjNPA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-express": {
+ "version": "0.47.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.47.0.tgz",
+ "integrity": "sha512-XFWVx6k0XlU8lu6cBlCa29ONtVt6ADEjmxtyAyeF2+rifk8uBJbk1La0yIVfI0DoKURGbaEDTNelaXG9l/lNNQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-fastify": {
+ "version": "0.44.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.44.1.tgz",
+ "integrity": "sha512-RoVeMGKcNttNfXMSl6W4fsYoCAYP1vi6ZAWIGhBY+o7R9Y0afA7f9JJL0j8LHbyb0P0QhSYk+6O56OwI2k4iRQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-fs": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.19.0.tgz",
+ "integrity": "sha512-JGwmHhBkRT2G/BYNV1aGI+bBjJu4fJUD/5/Jat0EWZa2ftrLV3YE8z84Fiij/wK32oMZ88eS8DI4ecLGZhpqsQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-generic-pool": {
+ "version": "0.43.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.43.0.tgz",
+ "integrity": "sha512-at8GceTtNxD1NfFKGAuwtqM41ot/TpcLh+YsGe4dhf7gvv1HW/ZWdq6nfRtS6UjIvZJOokViqLPJ3GVtZItAnQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-graphql": {
+ "version": "0.47.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.47.0.tgz",
+ "integrity": "sha512-Cc8SMf+nLqp0fi8oAnooNEfwZWFnzMiBHCGmDFYqmgjPylyLmi83b+NiTns/rKGwlErpW0AGPt0sMpkbNlzn8w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-hapi": {
+ "version": "0.45.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.45.1.tgz",
+ "integrity": "sha512-VH6mU3YqAKTePPfUPwfq4/xr049774qWtfTuJqVHoVspCLiT3bW+fCQ1toZxt6cxRPYASoYaBsMA3CWo8B8rcw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-http": {
+ "version": "0.57.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.57.1.tgz",
+ "integrity": "sha512-ThLmzAQDs7b/tdKI3BV2+yawuF09jF111OFsovqT1Qj3D8vjwKBwhi/rDE5xethwn4tSXtZcJ9hBsVAlWFQZ7g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.30.1",
+ "@opentelemetry/instrumentation": "0.57.1",
+ "@opentelemetry/semantic-conventions": "1.28.0",
+ "forwarded-parse": "2.1.2",
+ "semver": "^7.5.2"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-ioredis": {
+ "version": "0.47.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.47.0.tgz",
+ "integrity": "sha512-4HqP9IBC8e7pW9p90P3q4ox0XlbLGme65YTrA3UTLvqvo4Z6b0puqZQP203YFu8m9rE/luLfaG7/xrwwqMUpJw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/redis-common": "^0.36.2",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-kafkajs": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.7.0.tgz",
+ "integrity": "sha512-LB+3xiNzc034zHfCtgs4ITWhq6Xvdo8bsq7amR058jZlf2aXXDrN9SV4si4z2ya9QX4tz6r4eZJwDkXOp14/AQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-knex": {
+ "version": "0.44.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.44.0.tgz",
+ "integrity": "sha512-SlT0+bLA0Lg3VthGje+bSZatlGHw/vwgQywx0R/5u9QC59FddTQSPJeWNw29M6f8ScORMeUOOTwihlQAn4GkJQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-koa": {
+ "version": "0.47.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.47.0.tgz",
+ "integrity": "sha512-HFdvqf2+w8sWOuwtEXayGzdZ2vWpCKEQv5F7+2DSA74Te/Cv4rvb2E5So5/lh+ok4/RAIPuvCbCb/SHQFzMmbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-lru-memoizer": {
+ "version": "0.44.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.44.0.tgz",
+ "integrity": "sha512-Tn7emHAlvYDFik3vGU0mdwvWJDwtITtkJ+5eT2cUquct6nIs+H8M47sqMJkCpyPe5QIBJoTOHxmc6mj9lz6zDw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-mongodb": {
+ "version": "0.51.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.51.0.tgz",
+ "integrity": "sha512-cMKASxCX4aFxesoj3WK8uoQ0YUrRvnfxaO72QWI2xLu5ZtgX/QvdGBlU3Ehdond5eb74c2s1cqRQUIptBnKz1g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-mongoose": {
+ "version": "0.46.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.46.0.tgz",
+ "integrity": "sha512-mtVv6UeaaSaWTeZtLo4cx4P5/ING2obSqfWGItIFSunQBrYROfhuVe7wdIrFUs2RH1tn2YYpAJyMaRe/bnTTIQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-mysql": {
+ "version": "0.45.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.45.0.tgz",
+ "integrity": "sha512-tWWyymgwYcTwZ4t8/rLDfPYbOTF3oYB8SxnYMtIQ1zEf5uDm90Ku3i6U/vhaMyfHNlIHvDhvJh+qx5Nc4Z3Acg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0",
+ "@types/mysql": "2.15.26"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-mysql2": {
+ "version": "0.45.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.45.0.tgz",
+ "integrity": "sha512-qLslv/EPuLj0IXFvcE3b0EqhWI8LKmrgRPIa4gUd8DllbBpqJAvLNJSv3cC6vWwovpbSI3bagNO/3Q2SuXv2xA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0",
+ "@opentelemetry/sql-common": "^0.40.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-nestjs-core": {
+ "version": "0.44.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.44.0.tgz",
+ "integrity": "sha512-t16pQ7A4WYu1yyQJZhRKIfUNvl5PAaF2pEteLvgJb/BWdd1oNuU1rOYt4S825kMy+0q4ngiX281Ss9qiwHfxFQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-pg": {
+ "version": "0.50.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.50.0.tgz",
+ "integrity": "sha512-TtLxDdYZmBhFswm8UIsrDjh/HFBeDXd4BLmE8h2MxirNHewLJ0VS9UUddKKEverb5Sm2qFVjqRjcU+8Iw4FJ3w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.26.0",
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "1.27.0",
+ "@opentelemetry/sql-common": "^0.40.1",
+ "@types/pg": "8.6.1",
+ "@types/pg-pool": "2.0.6"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-pg/node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.27.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
+ "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-redis-4": {
+ "version": "0.46.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.46.0.tgz",
+ "integrity": "sha512-aTUWbzbFMFeRODn3720TZO0tsh/49T8H3h8vVnVKJ+yE36AeW38Uj/8zykQ/9nO8Vrtjr5yKuX3uMiG/W8FKNw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/redis-common": "^0.36.2",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-tedious": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.18.0.tgz",
+ "integrity": "sha512-9zhjDpUDOtD+coeADnYEJQ0IeLVCj7w/hqzIutdp5NqS1VqTAanaEfsEcSypyvYv5DX3YOsTUoF+nr2wDXPETA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0",
+ "@types/tedious": "^4.0.14"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-undici": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.10.0.tgz",
+ "integrity": "sha512-vm+V255NGw9gaSsPD6CP0oGo8L55BffBc8KnxqsMuc6XiAD1L8SFNzsW0RHhxJFqy9CJaJh+YiJ5EHXuZ5rZBw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.7.0"
+ }
+ },
+ "node_modules/@opentelemetry/redis-common": {
+ "version": "0.36.2",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz",
+ "integrity": "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/resources": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz",
+ "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.30.1",
+ "@opentelemetry/semantic-conventions": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz",
+ "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.30.1",
+ "@opentelemetry/resources": "1.30.1",
+ "@opentelemetry/semantic-conventions": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+ "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/sql-common": {
+ "version": "0.40.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz",
+ "integrity": "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0"
+ }
+ },
+ "node_modules/@oslojs/asn1": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@oslojs/asn1/-/asn1-1.0.0.tgz",
+ "integrity": "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@oslojs/binary": "1.0.0"
+ }
+ },
+ "node_modules/@oslojs/binary": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@oslojs/binary/-/binary-1.0.0.tgz",
+ "integrity": "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==",
+ "license": "MIT"
+ },
+ "node_modules/@oslojs/crypto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@oslojs/crypto/-/crypto-1.0.1.tgz",
+ "integrity": "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@oslojs/asn1": "1.0.0",
+ "@oslojs/binary": "1.0.0"
+ }
+ },
+ "node_modules/@oslojs/encoding": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz",
+ "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==",
+ "license": "MIT"
+ },
+ "node_modules/@paralleldrive/cuid2": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
+ "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "^1.1.5"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.50.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz",
+ "integrity": "sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.50.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@prisma/client": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.3.1.tgz",
+ "integrity": "sha512-ARAJaPs+eBkemdky/XU3cvGRl+mIPHCN2lCXsl5Vlb0E2gV+R6IN7aCI8CisRGszEZondwIsW9Iz8EJkTdykyA==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "peerDependencies": {
+ "prisma": "*",
+ "typescript": ">=5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "prisma": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@prisma/debug": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.3.1.tgz",
+ "integrity": "sha512-RrEBkd+HLZx+ydfmYT0jUj7wjLiS95wfTOSQ+8FQbvb6vHh5AeKfEPt/XUQ5+Buljj8hltEfOslEW57/wQIVeA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/engines": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.3.1.tgz",
+ "integrity": "sha512-sXdqEVLyGAJ5/iUoG/Ea5AdHMN71m6PzMBWRQnLmhhOejzqAaEr8rUd623ql6OJpED4s/U4vIn4dg1qkF7vGag==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "6.3.1",
+ "@prisma/engines-version": "6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0",
+ "@prisma/fetch-engine": "6.3.1",
+ "@prisma/get-platform": "6.3.1"
+ }
+ },
+ "node_modules/@prisma/engines-version": {
+ "version": "6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0.tgz",
+ "integrity": "sha512-R/ZcMuaWZT2UBmgX3Ko6PAV3f8//ZzsjRIG1eKqp3f2rqEqVtCv+mtzuH2rBPUC9ujJ5kCb9wwpxeyCkLcHVyA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/fetch-engine": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.3.1.tgz",
+ "integrity": "sha512-HOf/0umOgt+/S2xtZze+FHKoxpVg4YpVxROr6g2YG09VsI3Ipyb+rGvD6QGbCqkq5NTWAAZoOGNL+oy7t+IhaQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "6.3.1",
+ "@prisma/engines-version": "6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0",
+ "@prisma/get-platform": "6.3.1"
+ }
+ },
+ "node_modules/@prisma/get-platform": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.3.1.tgz",
+ "integrity": "sha512-AYLq6Hk9xG73JdLWJ3Ip9Wg/vlP7xPvftGBalsPzKDOHr/ImhwJ09eS8xC2vNT12DlzGxhfk8BkL0ve2OriNhQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "6.3.1"
+ }
+ },
+ "node_modules/@prisma/instrumentation": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.3.1.tgz",
+ "integrity": "sha512-XETCiBQYm8p/3ouNs0sv00MqjeYYM6sge0CSgx+Oevex99cfOBPt+J6JbOPyYqYcyOd3m2j2xRlXkPUKmpuxiA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.8"
+ }
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
+ "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
+ "integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz",
+ "integrity": "sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-presence": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "@radix-ui/react-use-previous": "1.1.0",
+ "@radix-ui/react-use-size": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
+ "integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-slot": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
+ "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
+ "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
+ "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.4.tgz",
+ "integrity": "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-escape-keydown": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.5.tgz",
+ "integrity": "sha512-50ZmEFL1kOuLalPKHrLWvPFMons2fGx9TqQCWlPwDVpbAnaUJ1g4XNcKqFNMQymYU0kKWR4MDDi+9vUQBGFgcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-menu": "2.1.5",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-controllable-state": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
+ "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz",
+ "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-callback-ref": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
+ "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz",
+ "integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.5.tgz",
+ "integrity": "sha512-uH+3w5heoMJtqVCgYOtYVMECk1TOrkUn0OG0p5MqXC0W2ppcuVeESbou8PTHoqAjbdTEK19AGXBWcEtR5WpEQg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-collection": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-dismissable-layer": "1.1.4",
+ "@radix-ui/react-focus-guards": "1.1.1",
+ "@radix-ui/react-focus-scope": "1.1.1",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-popper": "1.2.1",
+ "@radix-ui/react-portal": "1.1.3",
+ "@radix-ui/react-presence": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-roving-focus": "1.1.1",
+ "@radix-ui/react-slot": "1.1.1",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
+ "integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0",
+ "@radix-ui/react-use-rect": "1.1.0",
+ "@radix-ui/react-use-size": "1.1.0",
+ "@radix-ui/rect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz",
+ "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
+ "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
+ "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
+ "integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-collection": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
+ "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toast": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.5.tgz",
+ "integrity": "sha512-ZzUsAaOx8NdXZZKcFNDhbSlbsCUy8qQWmzTdgrlrhhZAOx2ofLtKrBDW9fkqhFvXgmtv560Uj16pkLkqML7SHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-collection": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.4",
+ "@radix-ui/react-portal": "1.1.3",
+ "@radix-ui/react-presence": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0",
+ "@radix-ui/react-visually-hidden": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.7.tgz",
+ "integrity": "sha512-ss0s80BC0+g0+Zc53MvilcnTYSOi4mSuFWBPYPuTOFGjx+pUU+ZrmamMNwS56t8MTFlniA5ocjd4jYm/CdhbOg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.4",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-popper": "1.2.1",
+ "@radix-ui/react-portal": "1.1.3",
+ "@radix-ui/react-presence": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-slot": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "@radix-ui/react-visually-hidden": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
+ "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
+ "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
+ "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
+ "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz",
+ "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
+ "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
+ "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz",
+ "integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
+ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
+ "license": "MIT"
+ },
+ "node_modules/@react-email/body": {
+ "version": "0.0.11",
+ "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.11.tgz",
+ "integrity": "sha512-ZSD2SxVSgUjHGrB0Wi+4tu3MEpB4fYSbezsFNEJk2xCWDBkFiOeEsjTmR5dvi+CxTK691hQTQlHv0XWuP7ENTg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/button": {
+ "version": "0.0.19",
+ "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.19.tgz",
+ "integrity": "sha512-HYHrhyVGt7rdM/ls6FuuD6XE7fa7bjZTJqB2byn6/oGsfiEZaogY77OtoLL/mrQHjHjZiJadtAMSik9XLcm7+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/code-block": {
+ "version": "0.0.11",
+ "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.11.tgz",
+ "integrity": "sha512-4D43p+LIMjDzm66gTDrZch0Flkip5je91mAT7iGs6+SbPyalHgIA+lFQoQwhz/VzHHLxuD0LV6gwmU/WUQ2WEg==",
+ "license": "MIT",
+ "dependencies": {
+ "prismjs": "1.29.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/code-inline": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.5.tgz",
+ "integrity": "sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/column": {
+ "version": "0.0.13",
+ "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.13.tgz",
+ "integrity": "sha512-Lqq17l7ShzJG/d3b1w/+lVO+gp2FM05ZUo/nW0rjxB8xBICXOVv6PqjDnn3FXKssvhO5qAV20lHM6S+spRhEwQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/components": {
+ "version": "0.0.32",
+ "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.32.tgz",
+ "integrity": "sha512-+1Wv7PyVgWfLoj5W0+CvBsJMIfMI6ibcFcIPXNkb2lhKQQASgxSoAedRL1rH0CCaBo6+63tg8y4baHzJonfZbw==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-email/body": "0.0.11",
+ "@react-email/button": "0.0.19",
+ "@react-email/code-block": "0.0.11",
+ "@react-email/code-inline": "0.0.5",
+ "@react-email/column": "0.0.13",
+ "@react-email/container": "0.0.15",
+ "@react-email/font": "0.0.9",
+ "@react-email/head": "0.0.12",
+ "@react-email/heading": "0.0.15",
+ "@react-email/hr": "0.0.11",
+ "@react-email/html": "0.0.11",
+ "@react-email/img": "0.0.11",
+ "@react-email/link": "0.0.12",
+ "@react-email/markdown": "0.0.14",
+ "@react-email/preview": "0.0.12",
+ "@react-email/render": "1.0.4",
+ "@react-email/row": "0.0.12",
+ "@react-email/section": "0.0.16",
+ "@react-email/tailwind": "1.0.4",
+ "@react-email/text": "0.0.11"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/container": {
+ "version": "0.0.15",
+ "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.15.tgz",
+ "integrity": "sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/font": {
+ "version": "0.0.9",
+ "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.9.tgz",
+ "integrity": "sha512-4zjq23oT9APXkerqeslPH3OZWuh5X4crHK6nx82mVHV2SrLba8+8dPEnWbaACWTNjOCbcLIzaC9unk7Wq2MIXw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/head": {
+ "version": "0.0.12",
+ "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.12.tgz",
+ "integrity": "sha512-X2Ii6dDFMF+D4niNwMAHbTkeCjlYYnMsd7edXOsi0JByxt9wNyZ9EnhFiBoQdqkE+SMDcu8TlNNttMrf5sJeMA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/heading": {
+ "version": "0.0.15",
+ "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.15.tgz",
+ "integrity": "sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/hr": {
+ "version": "0.0.11",
+ "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.11.tgz",
+ "integrity": "sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/html": {
+ "version": "0.0.11",
+ "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.11.tgz",
+ "integrity": "sha512-qJhbOQy5VW5qzU74AimjAR9FRFQfrMa7dn4gkEXKMB/S9xZN8e1yC1uA9C15jkXI/PzmJ0muDIWmFwatm5/+VA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/img": {
+ "version": "0.0.11",
+ "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.11.tgz",
+ "integrity": "sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/link": {
+ "version": "0.0.12",
+ "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.12.tgz",
+ "integrity": "sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/markdown": {
+ "version": "0.0.14",
+ "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.14.tgz",
+ "integrity": "sha512-5IsobCyPkb4XwnQO8uFfGcNOxnsg3311GRXhJ3uKv51P7Jxme4ycC/MITnwIZ10w2zx7HIyTiqVzTj4XbuIHbg==",
+ "license": "MIT",
+ "dependencies": {
+ "md-to-react-email": "5.0.5"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/preview": {
+ "version": "0.0.12",
+ "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.12.tgz",
+ "integrity": "sha512-g/H5fa9PQPDK6WUEG7iTlC19sAktI23qyoiJtMLqQiXFCfWeQMhqjLGKeLSKkfzszqmfJCjZtpSiKtBoOdxp3Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/render": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.4.tgz",
+ "integrity": "sha512-8ZXi89d8igBDE6W3zlHBa3GEDWKEUFDAa7i8MvVxnRViQuvsRbibK3ltuPgixxRI5+HgGNCSreBHQKZCkhUdyw==",
+ "license": "MIT",
+ "dependencies": {
+ "html-to-text": "9.0.5",
+ "prettier": "3.4.2",
+ "react-promise-suspense": "0.3.4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/row": {
+ "version": "0.0.12",
+ "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.12.tgz",
+ "integrity": "sha512-HkCdnEjvK3o+n0y0tZKXYhIXUNPDx+2vq1dJTmqappVHXS5tXS6W5JOPZr5j+eoZ8gY3PShI2LWj5rWF7ZEtIQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/section": {
+ "version": "0.0.16",
+ "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.16.tgz",
+ "integrity": "sha512-FjqF9xQ8FoeUZYKSdt8sMIKvoT9XF8BrzhT3xiFKdEMwYNbsDflcjfErJe3jb7Wj/es/lKTbV5QR1dnLzGpL3w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/tailwind": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-1.0.4.tgz",
+ "integrity": "sha512-tJdcusncdqgvTUYZIuhNC6LYTfL9vNTSQpwWdTCQhQ1lsrNCEE4OKCSdzSV3S9F32pi0i0xQ+YPJHKIzGjdTSA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-email/text": {
+ "version": "0.0.11",
+ "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.11.tgz",
+ "integrity": "sha512-a7nl/2KLpRHOYx75YbYZpWspUbX1DFY7JIZbOv5x0QU8SvwDbJt+Hm01vG34PffFyYvHEXrc6Qnip2RTjljNjg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@react-router/dev": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.1.5.tgz",
+ "integrity": "sha512-3YQAbaKQm4mxcd0jmbItr2Ik0GshEMmPpRAld7XhIymB50MklWSsgd+IJWcqSv8RVlNs1ZMxRC0maS3Hv/V19g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.21.8",
+ "@babel/generator": "^7.21.5",
+ "@babel/parser": "^7.21.8",
+ "@babel/plugin-syntax-decorators": "^7.22.10",
+ "@babel/plugin-syntax-jsx": "^7.21.4",
+ "@babel/preset-typescript": "^7.21.5",
+ "@babel/traverse": "^7.23.2",
+ "@babel/types": "^7.22.5",
+ "@npmcli/package-json": "^4.0.1",
+ "@react-router/node": "7.1.5",
+ "arg": "^5.0.1",
+ "babel-dead-code-elimination": "^1.0.6",
+ "chokidar": "^4.0.0",
+ "dedent": "^1.5.3",
+ "es-module-lexer": "^1.3.1",
+ "exit-hook": "2.2.1",
+ "fs-extra": "^10.0.0",
+ "gunzip-maybe": "^1.4.2",
+ "jsesc": "3.0.2",
+ "lodash": "^4.17.21",
+ "pathe": "^1.1.2",
+ "picocolors": "^1.1.1",
+ "picomatch": "^2.3.1",
+ "prettier": "^2.7.1",
+ "react-refresh": "^0.14.0",
+ "semver": "^7.3.7",
+ "set-cookie-parser": "^2.6.0",
+ "valibot": "^0.41.0",
+ "vite-node": "3.0.0-beta.2"
+ },
+ "bin": {
+ "react-router": "bin.js"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@react-router/serve": "^7.1.5",
+ "react-router": "7.1.5",
+ "typescript": "^5.1.0",
+ "vite": "^5.1.0 || ^6.0.0",
+ "wrangler": "^3.28.2"
+ },
+ "peerDependenciesMeta": {
+ "@react-router/serve": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ },
+ "wrangler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-router/dev/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@react-router/dev/node_modules/prettier": {
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+ "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin-prettier.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/@react-router/express": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/@react-router/express/-/express-7.1.5.tgz",
+ "integrity": "sha512-k9aGrvPwCP+8CeHPxRaIqYKJi3xVzdN4QXFdZ++PPcPNy5/g8pM7GBAxWyUYH26+aDO8AqjzgbGgph2H0MN7kQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-router/node": "7.1.5"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "express": "^4.17.1",
+ "react-router": "7.1.5",
+ "typescript": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-router/node": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.1.5.tgz",
+ "integrity": "sha512-Ga8xFHxO2yt5TpGwV5xYx4LC3eUDmhT6jYfTbMFb6F7hBA9sLdHxNfYZCe2WEfVZ4/BM7I8989Qzq6BWilV2LA==",
+ "license": "MIT",
+ "dependencies": {
+ "@mjackson/node-fetch-server": "^0.2.0",
+ "source-map-support": "^0.5.21",
+ "stream-slice": "^0.1.2",
+ "undici": "^6.19.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react-router": "7.1.5",
+ "typescript": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-router/remix-routes-option-adapter": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/@react-router/remix-routes-option-adapter/-/remix-routes-option-adapter-7.1.5.tgz",
+ "integrity": "sha512-iszgbGh5NewSr1xwK/ULcJ2/jkFyKZppzeil8AY8CNDGjaav/L/cVuSG+jkqK3wnSCudjhQyMkVwER2sCWRLUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@react-router/dev": "^7.1.5",
+ "typescript": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.22.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.22.0.tgz",
+ "integrity": "sha512-MBOl8MeOzpK0HQQQshKB7pABXbmyHizdTpqnrIseTbsv0nAepwC2ENZa1aaBExNQcpLoXmWthhak8SABLzvGPw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@remix-run/server-runtime": {
+ "version": "2.15.3",
+ "resolved": "https://registry.npmjs.org/@remix-run/server-runtime/-/server-runtime-2.15.3.tgz",
+ "integrity": "sha512-taHBe1DEqxZNjjj6OfkSYbup+sZPjbTgUhykaI+nHqrC2NDQuTiisBXhLwtx60GctONR/x0lWhF7R9ZGC5WsHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.22.0",
+ "@types/cookie": "^0.6.0",
+ "@web3-storage/multipart-parser": "^1.0.0",
+ "cookie": "^0.6.0",
+ "set-cookie-parser": "^2.4.8",
+ "source-map": "^0.7.3",
+ "turbo-stream": "2.4.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "typescript": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@remix-run/server-runtime/node_modules/@types/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
+ "license": "MIT"
+ },
+ "node_modules/@remix-run/server-runtime/node_modules/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@remix-run/server-runtime/node_modules/source-map": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
+ "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz",
+ "integrity": "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz",
+ "integrity": "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz",
+ "integrity": "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz",
+ "integrity": "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz",
+ "integrity": "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz",
+ "integrity": "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz",
+ "integrity": "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz",
+ "integrity": "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz",
+ "integrity": "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz",
+ "integrity": "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz",
+ "integrity": "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz",
+ "integrity": "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz",
+ "integrity": "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz",
+ "integrity": "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz",
+ "integrity": "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz",
+ "integrity": "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz",
+ "integrity": "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz",
+ "integrity": "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz",
+ "integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@sec-ant/readable-stream": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
+ "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
+ "license": "MIT"
+ },
+ "node_modules/@selderee/plugin-htmlparser2": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
+ "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "domhandler": "^5.0.3",
+ "selderee": "^0.11.0"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/killymxi"
+ }
+ },
+ "node_modules/@sentry-internal/browser-utils": {
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.54.0.tgz",
+ "integrity": "sha512-DKWCqb4YQosKn6aD45fhKyzhkdG7N6goGFDeyTaJFREJDFVDXiNDsYZu30nJ6BxMM7uQIaARhPAC5BXfoED3pQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "8.54.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "node_modules/@sentry-internal/feedback": {
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.54.0.tgz",
+ "integrity": "sha512-nQqRacOXoElpE0L0ADxUUII0I3A94niqG9Z4Fmsw6057QvyrV/LvTiMQBop6r5qLjwMqK+T33iR4/NQI5RhsXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "8.54.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "node_modules/@sentry-internal/replay": {
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.54.0.tgz",
+ "integrity": "sha512-8xuBe06IaYIGJec53wUC12tY2q4z2Z0RPS2s1sLtbA00EvK1YDGuXp96IDD+HB9mnDMrQ/jW5f97g9TvPsPQUg==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry-internal/browser-utils": "8.54.0",
+ "@sentry/core": "8.54.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "node_modules/@sentry-internal/replay-canvas": {
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.54.0.tgz",
+ "integrity": "sha512-K/On3OAUBeq/TV2n+1EvObKC+WMV9npVXpVyJqCCyn8HYMm8FUGzuxeajzm0mlW4wDTPCQor6mK9/IgOquUzCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry-internal/replay": "8.54.0",
+ "@sentry/core": "8.54.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "node_modules/@sentry/babel-plugin-component-annotate": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.1.2.tgz",
+ "integrity": "sha512-5h2WXRJ6swKA0TwxHHryC8M2QyOfS9QhTAL6ElPfkEYe9HhJieXmxsDpyspbqAa26ccnCUcmwE5vL34jAjt4sQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@sentry/browser": {
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.54.0.tgz",
+ "integrity": "sha512-BgUtvxFHin0fS0CmJVKTLXXZcke0Av729IVfi+2fJ4COX8HO7/HAP02RKaSQGmL2HmvWYTfNZ7529AnUtrM4Rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry-internal/browser-utils": "8.54.0",
+ "@sentry-internal/feedback": "8.54.0",
+ "@sentry-internal/replay": "8.54.0",
+ "@sentry-internal/replay-canvas": "8.54.0",
+ "@sentry/core": "8.54.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.1.2.tgz",
+ "integrity": "sha512-lqOCvmOPzKiQenIMhmm5/mwCntwFy0dPZbVD28Dnr3MXpT1rIBg1HXjfnqQWFlMRbL9haSsWiY/TQyR/6b30YA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.18.5",
+ "@sentry/babel-plugin-component-annotate": "3.1.2",
+ "@sentry/cli": "2.41.1",
+ "dotenv": "^16.3.1",
+ "find-up": "^5.0.0",
+ "glob": "^9.3.2",
+ "magic-string": "0.30.8",
+ "unplugin": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/glob": {
+ "version": "9.3.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz",
+ "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "minimatch": "^8.0.2",
+ "minipass": "^4.2.4",
+ "path-scurry": "^1.6.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/minimatch": {
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz",
+ "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/minipass": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz",
+ "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/path-scurry/node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/@sentry/cli": {
+ "version": "2.41.1",
+ "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.41.1.tgz",
+ "integrity": "sha512-0GVmDiTV7R1492wkVY4bGcfC0fSmRmQjuxaaPI8CIV9B2VP9pBVCUizi1mevXaaE4I3fM60LI+XYrKFEneuVog==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "https-proxy-agent": "^5.0.0",
+ "node-fetch": "^2.6.7",
+ "progress": "^2.0.3",
+ "proxy-from-env": "^1.1.0",
+ "which": "^2.0.2"
+ },
+ "bin": {
+ "sentry-cli": "bin/sentry-cli"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@sentry/cli-darwin": "2.41.1",
+ "@sentry/cli-linux-arm": "2.41.1",
+ "@sentry/cli-linux-arm64": "2.41.1",
+ "@sentry/cli-linux-i686": "2.41.1",
+ "@sentry/cli-linux-x64": "2.41.1",
+ "@sentry/cli-win32-i686": "2.41.1",
+ "@sentry/cli-win32-x64": "2.41.1"
+ }
+ },
+ "node_modules/@sentry/cli-darwin": {
+ "version": "2.41.1",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.41.1.tgz",
+ "integrity": "sha512-7pS3pu/SuhE6jOn3wptstAg6B5nUP878O6s+2svT7b5fKNfYUi/6NPK6dAveh2Ca0rwVq40TO4YFJabWMgTpdQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-linux-arm": {
+ "version": "2.41.1",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.41.1.tgz",
+ "integrity": "sha512-wNUvquD6qjOCczvuBGf9OiD29nuQ6yf8zzfyPJa5Bdx1QXuteKsKb6HBrMwuIR3liyuu0duzHd+H/+p1n541Hg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "linux",
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-linux-arm64": {
+ "version": "2.41.1",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.41.1.tgz",
+ "integrity": "sha512-EzYCEnnENBnS5kpNW+2dBcrPZn1MVfywh2joGVQZTpmgDL5YFJ59VOd+K0XuEwqgFI8BSNI14KXZ75s4DD1/Vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "linux",
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-linux-i686": {
+ "version": "2.41.1",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.41.1.tgz",
+ "integrity": "sha512-urpQCWrdYnSAsZY3udttuMV88wTJzKZL10xsrp7sjD/Hd+O6qSLVLkxebIlxts70jMLLFHYrQ2bkRg5kKuX6Fg==",
+ "cpu": [
+ "x86",
+ "ia32"
+ ],
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "linux",
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-linux-x64": {
+ "version": "2.41.1",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.41.1.tgz",
+ "integrity": "sha512-ZqpYwHXAaK4MMEFlyaLYr6mJTmpy9qP6n30jGhLTW7kHKS3s6GPLCSlNmIfeClrInEt0963fM633ZRnXa04VPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "linux",
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-win32-i686": {
+ "version": "2.41.1",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.41.1.tgz",
+ "integrity": "sha512-AuRimCeVsx99DIOr9cwdYBHk39tlmAuPDdy2r16iNzY0InXs4xOys4gGzM7N4vlFQvFkzuc778Su0HkfasgprA==",
+ "cpu": [
+ "x86",
+ "ia32"
+ ],
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-win32-x64": {
+ "version": "2.41.1",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.41.1.tgz",
+ "integrity": "sha512-6JcPvXGye61+wPp0xdzfc2YLE/Dcud8JdaK8VxLM3b/8+Em7E+UyliDu3uF8+YGUqizY5JYTd3fs17DC8DZhLw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@sentry/core": {
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.54.0.tgz",
+ "integrity": "sha512-03bWf+D1j28unOocY/5FDB6bUHtYlm6m6ollVejhg45ZmK9iPjdtxNWbrLsjT1WRym0Tjzowu+A3p+eebYEv0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "node_modules/@sentry/node": {
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@sentry/node/-/node-8.54.0.tgz",
+ "integrity": "sha512-z9ak481OtCw3V4l55ke/9FOiorF2J/niO1J1gvGefXpgFucpw0M3qqEFjB5cpg9HoZM8Y1WtA1OFusfTAnvcXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/context-async-hooks": "^1.30.1",
+ "@opentelemetry/core": "^1.30.1",
+ "@opentelemetry/instrumentation": "^0.57.1",
+ "@opentelemetry/instrumentation-amqplib": "^0.46.0",
+ "@opentelemetry/instrumentation-connect": "0.43.0",
+ "@opentelemetry/instrumentation-dataloader": "0.16.0",
+ "@opentelemetry/instrumentation-express": "0.47.0",
+ "@opentelemetry/instrumentation-fastify": "0.44.1",
+ "@opentelemetry/instrumentation-fs": "0.19.0",
+ "@opentelemetry/instrumentation-generic-pool": "0.43.0",
+ "@opentelemetry/instrumentation-graphql": "0.47.0",
+ "@opentelemetry/instrumentation-hapi": "0.45.1",
+ "@opentelemetry/instrumentation-http": "0.57.1",
+ "@opentelemetry/instrumentation-ioredis": "0.47.0",
+ "@opentelemetry/instrumentation-kafkajs": "0.7.0",
+ "@opentelemetry/instrumentation-knex": "0.44.0",
+ "@opentelemetry/instrumentation-koa": "0.47.0",
+ "@opentelemetry/instrumentation-lru-memoizer": "0.44.0",
+ "@opentelemetry/instrumentation-mongodb": "0.51.0",
+ "@opentelemetry/instrumentation-mongoose": "0.46.0",
+ "@opentelemetry/instrumentation-mysql": "0.45.0",
+ "@opentelemetry/instrumentation-mysql2": "0.45.0",
+ "@opentelemetry/instrumentation-nestjs-core": "0.44.0",
+ "@opentelemetry/instrumentation-pg": "0.50.0",
+ "@opentelemetry/instrumentation-redis-4": "0.46.0",
+ "@opentelemetry/instrumentation-tedious": "0.18.0",
+ "@opentelemetry/instrumentation-undici": "0.10.0",
+ "@opentelemetry/resources": "^1.30.1",
+ "@opentelemetry/sdk-trace-base": "^1.30.1",
+ "@opentelemetry/semantic-conventions": "^1.28.0",
+ "@prisma/instrumentation": "5.22.0",
+ "@sentry/core": "8.54.0",
+ "@sentry/opentelemetry": "8.54.0",
+ "import-in-the-middle": "^1.11.2"
+ },
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "node_modules/@sentry/node/node_modules/@opentelemetry/api-logs": {
+ "version": "0.53.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz",
+ "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@sentry/node/node_modules/@prisma/instrumentation": {
+ "version": "5.22.0",
+ "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-5.22.0.tgz",
+ "integrity": "sha512-LxccF392NN37ISGxIurUljZSh1YWnphO34V5a0+T7FVQG2u9bhAXRTJpgmQ3483woVhkraQZFF7cbRrpbw/F4Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.8",
+ "@opentelemetry/instrumentation": "^0.49 || ^0.50 || ^0.51 || ^0.52.0 || ^0.53.0",
+ "@opentelemetry/sdk-trace-base": "^1.22"
+ }
+ },
+ "node_modules/@sentry/node/node_modules/@prisma/instrumentation/node_modules/@opentelemetry/instrumentation": {
+ "version": "0.53.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz",
+ "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.53.0",
+ "@types/shimmer": "^1.2.0",
+ "import-in-the-middle": "^1.8.1",
+ "require-in-the-middle": "^7.1.1",
+ "semver": "^7.5.2",
+ "shimmer": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@sentry/opentelemetry": {
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-8.54.0.tgz",
+ "integrity": "sha512-Tkmd8bmXMx0PKZF53ywk/FfvDrphX8NdPH5N53HxyMvGxSf2trZkTuOSFJg6zKibyGYO6+PUeGO3g2WJKUxwGA==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "8.54.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/context-async-hooks": "^1.30.1",
+ "@opentelemetry/core": "^1.30.1",
+ "@opentelemetry/instrumentation": "^0.57.1",
+ "@opentelemetry/sdk-trace-base": "^1.30.1",
+ "@opentelemetry/semantic-conventions": "^1.28.0"
+ }
+ },
+ "node_modules/@sentry/profiling-node": {
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@sentry/profiling-node/-/profiling-node-8.54.0.tgz",
+ "integrity": "sha512-E2WKaEmQunu1PiUX1HGYVwEw18b4YDBLINeOTwJRqApGSNwc/EggYZzhXSPWjsPh7D5ZRGBr0p1bKQwkNMvtRg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "8.54.0",
+ "@sentry/node": "8.54.0",
+ "detect-libc": "^2.0.2",
+ "node-abi": "^3.61.0"
+ },
+ "bin": {
+ "sentry-prune-profiler-binaries": "scripts/prune-profiler-binaries.js"
+ },
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "node_modules/@sentry/react": {
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@sentry/react/-/react-8.54.0.tgz",
+ "integrity": "sha512-42T/fp8snYN19Fy/2P0Mwotu4gcdy+1Lx+uYCNcYP1o7wNGigJ7qb27sW7W34GyCCHjoCCfQgeOqDQsyY8LC9w==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/browser": "8.54.0",
+ "@sentry/core": "8.54.0",
+ "hoist-non-react-statics": "^3.3.2"
+ },
+ "engines": {
+ "node": ">=14.18"
+ },
+ "peerDependencies": {
+ "react": "^16.14.0 || 17.x || 18.x || 19.x"
+ }
+ },
+ "node_modules/@sentry/vite-plugin": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-3.1.2.tgz",
+ "integrity": "sha512-a927sabQKviA4PAs9cM3rFONHiVdfEHHkypmub+hFwJNL0sbeg/8uht0WyqDT5WjVT5pbyvLaKLDjGdwrRBY6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/bundler-plugin-core": "3.1.2",
+ "unplugin": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@sindresorhus/merge-streams": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
+ "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@sly-cli/sly": {
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/@sly-cli/sly/-/sly-1.14.0.tgz",
+ "integrity": "sha512-hczYYDeTUU+CMTMMcfH4MTN9dLyzivfAISI0EyTHLzHHJnTSOWYqJwyNRib+/yrSumdDMSym4n8zBRZEhJpyBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@epic-web/cachified": "^5.1.2",
+ "chalk": "^5.3.0",
+ "commander": "^11.0.0",
+ "compare-versions": "^6.0.0",
+ "cosmiconfig": "^8.2.0",
+ "esbuild": "^0.19.11",
+ "execa": "^7.1.1",
+ "lru-cache": "^10.0.0",
+ "ora": "^6.3.1",
+ "prompts": "^2.4.2",
+ "zod": "^3.21.4"
+ },
+ "bin": {
+ "sly": "start.js"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
+ "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/android-arm": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
+ "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/android-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
+ "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/android-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
+ "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
+ "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/darwin-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
+ "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
+ "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
+ "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/linux-arm": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
+ "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/linux-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
+ "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/linux-ia32": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
+ "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/linux-loong64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
+ "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
+ "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
+ "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
+ "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/linux-s390x": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
+ "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/linux-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
+ "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
+ "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
+ "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/sunos-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
+ "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/win32-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
+ "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/win32-ia32": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
+ "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/@esbuild/win32-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
+ "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/esbuild": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
+ "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.19.12",
+ "@esbuild/android-arm": "0.19.12",
+ "@esbuild/android-arm64": "0.19.12",
+ "@esbuild/android-x64": "0.19.12",
+ "@esbuild/darwin-arm64": "0.19.12",
+ "@esbuild/darwin-x64": "0.19.12",
+ "@esbuild/freebsd-arm64": "0.19.12",
+ "@esbuild/freebsd-x64": "0.19.12",
+ "@esbuild/linux-arm": "0.19.12",
+ "@esbuild/linux-arm64": "0.19.12",
+ "@esbuild/linux-ia32": "0.19.12",
+ "@esbuild/linux-loong64": "0.19.12",
+ "@esbuild/linux-mips64el": "0.19.12",
+ "@esbuild/linux-ppc64": "0.19.12",
+ "@esbuild/linux-riscv64": "0.19.12",
+ "@esbuild/linux-s390x": "0.19.12",
+ "@esbuild/linux-x64": "0.19.12",
+ "@esbuild/netbsd-x64": "0.19.12",
+ "@esbuild/openbsd-x64": "0.19.12",
+ "@esbuild/sunos-x64": "0.19.12",
+ "@esbuild/win32-arm64": "0.19.12",
+ "@esbuild/win32-ia32": "0.19.12",
+ "@esbuild/win32-x64": "0.19.12"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/execa": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz",
+ "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.1",
+ "human-signals": "^4.3.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^3.0.7",
+ "strip-final-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || ^16.14.0 || >=18.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/human-signals": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
+ "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14.18.0"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@sly-cli/sly/node_modules/mimic-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/npm-run-path": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
+ "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/onetime": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@sly-cli/sly/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@sly-cli/sly/node_modules/strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
+ "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "chalk": "^4.1.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/dom/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@testing-library/dom/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@testing-library/dom/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/@testing-library/dom/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/jest-dom": {
+ "version": "6.6.3",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
+ "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@adobe/css-tools": "^4.4.0",
+ "aria-query": "^5.0.0",
+ "chalk": "^3.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.6.3",
+ "lodash": "^4.17.21",
+ "redent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.2.0.tgz",
+ "integrity": "sha512-2cSskAvA1QNtKc8Y9VJQRv0tm3hLVgxRGDB+KYhIaPQJ1I+RHbhIXcM+zClKXzMes/wshsMVzf4B9vS4IZpqDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@testing-library/user-event": {
+ "version": "14.6.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
+ "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
+ }
+ },
+ "node_modules/@total-typescript/ts-reset": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/@total-typescript/ts-reset/-/ts-reset-0.6.1.tgz",
+ "integrity": "sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tusbar/cache-control": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@tusbar/cache-control/-/cache-control-1.0.2.tgz",
+ "integrity": "sha512-PXfjYTYBVvMPYCLDWj+xIOA9ITFbbhWCHzLcqUCJ5TPGm4JO4cxpGb7x3K8Q1K1ADgNgfBxLsDcTMVRydtZB9A=="
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.6.8",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz",
+ "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.20.6",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz",
+ "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.20.7"
+ }
+ },
+ "node_modules/@types/bcryptjs": {
+ "version": "2.4.6",
+ "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
+ "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/better-sqlite3": {
+ "version": "7.6.12",
+ "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.12.tgz",
+ "integrity": "sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.5",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
+ "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/compression": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz",
+ "integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.36",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz",
+ "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/doctrine": {
+ "version": "0.0.9",
+ "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz",
+ "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/eslint": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
+ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/express": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
+ "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "4.19.6",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
+ "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/fs-extra": {
+ "version": "11.0.4",
+ "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
+ "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/jsonfile": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/minimatch": "^5.1.2",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
+ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/jsonfile": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz",
+ "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/minimatch": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
+ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/morgan": {
+ "version": "1.9.9",
+ "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz",
+ "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/mysql": {
+ "version": "2.15.26",
+ "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz",
+ "integrity": "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "22.13.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz",
+ "integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.20.0"
+ }
+ },
+ "node_modules/@types/pg": {
+ "version": "8.6.1",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz",
+ "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^2.2.0"
+ }
+ },
+ "node_modules/@types/pg-pool": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz",
+ "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/pg": "*"
+ }
+ },
+ "node_modules/@types/qrcode": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz",
+ "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.9.17",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
+ "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.0.8",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz",
+ "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.0.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz",
+ "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.0.0"
+ }
+ },
+ "node_modules/@types/send": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
+ "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.7",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
+ "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/set-cookie-parser": {
+ "version": "2.4.10",
+ "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz",
+ "integrity": "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/shimmer": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
+ "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/source-map-support": {
+ "version": "0.5.10",
+ "resolved": "https://registry.npmjs.org/@types/source-map-support/-/source-map-support-0.5.10.tgz",
+ "integrity": "sha512-tgVP2H469x9zq34Z0m/fgPewGhg/MLClalNOiPIzQlXrSS2YrKu/xCdSCKnEDwkFha51VKEKB6A9wW26/ZNwzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/@types/statuses": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz",
+ "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/tedious": {
+ "version": "4.0.14",
+ "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz",
+ "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/tough-cookie": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
+ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.0.tgz",
+ "integrity": "sha512-NggSaEZCdSrFddbctrVjkVZvFC6KGfKfNK0CU7mNK/iKHGKbzT4Wmgm08dKpcZECBu9f5FypndoMyRHkdqfT1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.19.0",
+ "@typescript-eslint/type-utils": "8.19.0",
+ "@typescript-eslint/utils": "8.19.0",
+ "@typescript-eslint/visitor-keys": "8.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.3.1",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.8.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.0.tgz",
+ "integrity": "sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.19.0",
+ "@typescript-eslint/types": "8.19.0",
+ "@typescript-eslint/typescript-estree": "8.19.0",
+ "@typescript-eslint/visitor-keys": "8.19.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.8.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz",
+ "integrity": "sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.19.0",
+ "@typescript-eslint/visitor-keys": "8.19.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.0.tgz",
+ "integrity": "sha512-TZs0I0OSbd5Aza4qAMpp1cdCYVnER94IziudE3JU328YUHgWu9gwiwhag+fuLeJ2LkWLXI+F/182TbG+JaBdTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "8.19.0",
+ "@typescript-eslint/utils": "8.19.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.8.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz",
+ "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.0.tgz",
+ "integrity": "sha512-WW9PpDaLIFW9LCbucMSdYUuGeFUz1OkWYS/5fwZwTA+l2RwlWFdJvReQqMUMBw4yJWJOfqd7An9uwut2Oj8sLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.19.0",
+ "@typescript-eslint/visitor-keys": "8.19.0",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.8.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.0.tgz",
+ "integrity": "sha512-PTBG+0oEMPH9jCZlfg07LCB2nYI0I317yyvXGfxnvGvw4SHIOuRnQ3kadyyXY6tGdChusIHIbM5zfIbp4M6tCg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@typescript-eslint/scope-manager": "8.19.0",
+ "@typescript-eslint/types": "8.19.0",
+ "@typescript-eslint/typescript-estree": "8.19.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.8.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.0.tgz",
+ "integrity": "sha512-mCFtBbFBJDCNCWUl5y6sZSCHXw1DEFEk3c/M3nRK2a4XUB8StGFtmcEMizdjKuBzB6e/smJAAWYug3VrdLMr1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.19.0",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
+ "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.26.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.25.9",
+ "@babel/plugin-transform-react-jsx-source": "^7.25.9",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.14.2"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
+ }
+ },
+ "node_modules/@vitest/coverage-v8": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.5.tgz",
+ "integrity": "sha512-zOOWIsj5fHh3jjGwQg+P+J1FW3s4jBu1Zqga0qW60yutsBtqEqNEJKWYh7cYn1yGD+1bdPsPdC/eL4eVK56xMg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.3.0",
+ "@bcoe/v8-coverage": "^1.0.2",
+ "debug": "^4.4.0",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-lib-source-maps": "^5.0.6",
+ "istanbul-reports": "^3.1.7",
+ "magic-string": "^0.30.17",
+ "magicast": "^0.3.5",
+ "std-env": "^3.8.0",
+ "test-exclude": "^7.0.1",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "3.0.5",
+ "vitest": "3.0.5"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/coverage-v8/node_modules/magic-string": {
+ "version": "0.30.17",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
+ "node_modules/@vitest/eslint-plugin": {
+ "version": "1.1.22",
+ "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.22.tgz",
+ "integrity": "sha512-ztvy2+thiCMmKnywvKGhH3AcKgEMGd4BsFK2QC9/EXqlyjXDp7Pg96PonbLx8bDvNCAjq4hfCw5YuZSAz1EDIg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@typescript-eslint/utils": ">= 8.0",
+ "eslint": ">= 8.57.0",
+ "typescript": ">= 5.0.0",
+ "vitest": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ },
+ "vitest": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz",
+ "integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "3.0.5",
+ "@vitest/utils": "3.0.5",
+ "chai": "^5.1.2",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz",
+ "integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "3.0.5",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/mocker/node_modules/magic-string": {
+ "version": "0.30.17",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz",
+ "integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz",
+ "integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.0.5",
+ "pathe": "^2.0.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner/node_modules/pathe": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz",
+ "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz",
+ "integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.0.5",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot/node_modules/magic-string": {
+ "version": "0.30.17",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
+ "node_modules/@vitest/snapshot/node_modules/pathe": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz",
+ "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitest/spy": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz",
+ "integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^3.0.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz",
+ "integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.0.5",
+ "loupe": "^3.1.2",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@web3-storage/multipart-parser": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@web3-storage/multipart-parser/-/multipart-parser-1.0.0.tgz",
+ "integrity": "sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==",
+ "license": "(Apache-2.0 AND MIT)"
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
+ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-import-attributes": {
+ "version": "1.9.5",
+ "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
+ "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^8"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/address": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/address/-/address-2.0.3.tgz",
+ "integrity": "sha512-XNAb/a6TCqou+TufU8/u11HCu9x1gYvOoxLwtlXgIqmkrYQADVv6ljyW2zwiPhHz9R1gItAWpuDrdJMmrOBFEA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-escapes/node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/aria-hidden": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz",
+ "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
+ "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlast": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
+ "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+ "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+ "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
+ "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3",
+ "es-errors": "^1.3.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.20",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
+ "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.23.3",
+ "caniuse-lite": "^1.0.30001646",
+ "fraction.js": "^4.3.7",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.0.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/babel-dead-code-elimination": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.8.tgz",
+ "integrity": "sha512-og6HQERk0Cmm+nTT4Od2wbPtgABXFMPaHACjbKLulZIFMkYyXZLkUGuAxdgpMJBrxyt/XFpSz++lNzjbcMnPkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.23.7",
+ "@babel/parser": "^7.23.6",
+ "@babel/traverse": "^7.23.7",
+ "@babel/types": "^7.23.6"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/base32-decode": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/base32-decode/-/base32-decode-1.0.0.tgz",
+ "integrity": "sha512-KNWUX/R7wKenwE/G/qFMzGScOgVntOmbE27vvc6GrniDGYb6a5+qWcuoXl8WIOQL7q0TpK7nZDm1Y04Yi3Yn5g==",
+ "license": "MIT"
+ },
+ "node_modules/base32-encode": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base32-encode/-/base32-encode-2.0.0.tgz",
+ "integrity": "sha512-mlmkfc2WqdDtMl/id4qm3A7RjW6jxcbAoMjdRmsPiwQP0ufD4oXItYMnPgVHe80lnAIy+1xwzhHE1s4FoIceSw==",
+ "license": "MIT",
+ "dependencies": {
+ "to-data-view": "^2.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/basic-auth/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/bcryptjs": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
+ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
+ "license": "MIT"
+ },
+ "node_modules/better-sqlite3": {
+ "version": "11.8.1",
+ "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.1.tgz",
+ "integrity": "sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "bindings": "^1.5.0",
+ "prebuild-install": "^7.1.1"
+ }
+ },
+ "node_modules/big-integer": {
+ "version": "1.6.52",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
+ "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
+ "dev": true,
+ "license": "Unlicense",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "node_modules/bl": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz",
+ "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^6.0.3",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bl/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserify-zlib": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz",
+ "integrity": "sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pako": "~0.2.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.24.3",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
+ "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001688",
+ "electron-to-chromium": "^1.5.73",
+ "node-releases": "^2.0.19",
+ "update-browserslist-db": "^1.1.1"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT"
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
+ "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
+ "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001690",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
+ "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chai": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz",
+ "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
+ "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
+ "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "license": "ISC"
+ },
+ "node_modules/cjs-module-lexer": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz",
+ "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==",
+ "license": "MIT"
+ },
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
+ "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-spinners": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-width": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
+ "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/cliui/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/cliui/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cliui/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/close-with-grace": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/close-with-grace/-/close-with-grace-2.2.0.tgz",
+ "integrity": "sha512-OdcFxnxTm/AMLPHA4Aq3J1BLpkojXP7I4G5QBQLN5TT55ED/rk04rAoDbtfNnfZ988kGXPxh1bdRLeIU9bz/lA==",
+ "license": "MIT"
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
+ "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/compare-versions": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
+ "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz",
+ "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "compressible": "~2.0.18",
+ "debug": "2.6.9",
+ "negotiator": "~0.6.4",
+ "on-headers": "~1.0.2",
+ "safe-buffer": "5.2.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/compression/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/compression/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
+ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "license": "MIT"
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cosmiconfig": {
+ "version": "8.3.6",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
+ "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0",
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cross-env": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "bin": {
+ "cross-env": "src/bin/cross-env.js",
+ "cross-env-shell": "src/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=10.14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cross-spawn/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css-select": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
+ "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
+ "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cssstyle": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz",
+ "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "rrweb-cssom": "^0.7.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/inspect-js"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/date-fns": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
+ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/dedent": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz",
+ "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "babel-plugin-macros": "^3.1.0"
+ },
+ "peerDependenciesMeta": {
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/defaults": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
+ "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
+ "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/dijkstrajs": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
+ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
+ "license": "MIT"
+ },
+ "node_modules/discontinuous-range": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz",
+ "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "license": "MIT"
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.1.tgz",
+ "integrity": "sha512-xWXmuRnN9OMP6ptPd2+H0cCbcYBULa5YDTbMm/2lvkWvNA3O4wcW+GvzooqBuNM8yy6pl3VIAeJTUUWUbfI5Fw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.4.7",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
+ "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/duplexify": {
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
+ "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.0.0",
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.0.0",
+ "stream-shift": "^1.0.0"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "license": "MIT"
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.76",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.76.tgz",
+ "integrity": "sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==",
+ "license": "ISC"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/enforce-unique": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/enforce-unique/-/enforce-unique-1.3.0.tgz",
+ "integrity": "sha512-VaNJOwgNeOtZ4qvfGO5OsRWnbvF3jX0/v/3I2YSFpOnPwWj/spbDz1Ktbi5Z9v/eGgcqCqeghW1wkCnSWF4jcg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.18.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz",
+ "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.23.8",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.8.tgz",
+ "integrity": "sha512-lfab8IzDn6EpI1ibZakcgS6WsfEBiB+43cuJo+wgylx1xKXf+Sp+YR3vFuQwC/u3sxYwV8Cxe3B0DpVUu/WiJQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.2",
+ "arraybuffer.prototype.slice": "^1.0.4",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "data-view-buffer": "^1.0.2",
+ "data-view-byte-length": "^1.0.2",
+ "data-view-byte-offset": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-set-tostringtag": "^2.0.3",
+ "es-to-primitive": "^1.3.0",
+ "function.prototype.name": "^1.1.8",
+ "get-intrinsic": "^1.2.6",
+ "get-symbol-description": "^1.1.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.1.0",
+ "is-array-buffer": "^3.0.5",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.2",
+ "is-regex": "^1.2.1",
+ "is-shared-array-buffer": "^1.0.4",
+ "is-string": "^1.1.1",
+ "is-typed-array": "^1.1.15",
+ "is-weakref": "^1.1.0",
+ "math-intrinsics": "^1.1.0",
+ "object-inspect": "^1.13.3",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.7",
+ "own-keys": "^1.0.0",
+ "regexp.prototype.flags": "^1.5.3",
+ "safe-array-concat": "^1.1.3",
+ "safe-push-apply": "^1.0.0",
+ "safe-regex-test": "^1.1.0",
+ "string.prototype.trim": "^1.2.10",
+ "string.prototype.trimend": "^1.0.9",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.3",
+ "typed-array-byte-length": "^1.0.3",
+ "typed-array-byte-offset": "^1.0.4",
+ "typed-array-length": "^1.0.7",
+ "unbox-primitive": "^1.1.0",
+ "which-typed-array": "^1.1.18"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-iterator-helpers": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz",
+ "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.6",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.0.3",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.6",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "iterator.prototype": "^1.1.4",
+ "safe-array-concat": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
+ "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
+ "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
+ "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.4",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz",
+ "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.0"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
+ "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7",
+ "is-date-object": "^1.0.5",
+ "is-symbol": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
+ "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.24.2",
+ "@esbuild/android-arm": "0.24.2",
+ "@esbuild/android-arm64": "0.24.2",
+ "@esbuild/android-x64": "0.24.2",
+ "@esbuild/darwin-arm64": "0.24.2",
+ "@esbuild/darwin-x64": "0.24.2",
+ "@esbuild/freebsd-arm64": "0.24.2",
+ "@esbuild/freebsd-x64": "0.24.2",
+ "@esbuild/linux-arm": "0.24.2",
+ "@esbuild/linux-arm64": "0.24.2",
+ "@esbuild/linux-ia32": "0.24.2",
+ "@esbuild/linux-loong64": "0.24.2",
+ "@esbuild/linux-mips64el": "0.24.2",
+ "@esbuild/linux-ppc64": "0.24.2",
+ "@esbuild/linux-riscv64": "0.24.2",
+ "@esbuild/linux-s390x": "0.24.2",
+ "@esbuild/linux-x64": "0.24.2",
+ "@esbuild/netbsd-arm64": "0.24.2",
+ "@esbuild/netbsd-x64": "0.24.2",
+ "@esbuild/openbsd-arm64": "0.24.2",
+ "@esbuild/openbsd-x64": "0.24.2",
+ "@esbuild/sunos-x64": "0.24.2",
+ "@esbuild/win32-arm64": "0.24.2",
+ "@esbuild/win32-ia32": "0.24.2",
+ "@esbuild/win32-x64": "0.24.2"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.19.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz",
+ "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.19.0",
+ "@eslint/core": "^0.10.0",
+ "@eslint/eslintrc": "^3.2.0",
+ "@eslint/js": "9.19.0",
+ "@eslint/plugin-kit": "^0.2.5",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.1",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.2.0",
+ "eslint-visitor-keys": "^4.2.0",
+ "espree": "^10.3.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
+ "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.13.0",
+ "resolve": "^1.22.4"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import-x": {
+ "version": "4.6.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.6.1.tgz",
+ "integrity": "sha512-wluSUifMIb7UfwWXqx7Yx0lE/SGCcGXECLx/9bCmbY2nneLwvAZ4vkd1IXDjPKFvdcdUgr1BaRnaRpx3k2+Pfw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/doctrine": "^0.0.9",
+ "@typescript-eslint/scope-manager": "^8.1.0",
+ "@typescript-eslint/utils": "^8.1.0",
+ "debug": "^4.3.4",
+ "doctrine": "^3.0.0",
+ "enhanced-resolve": "^5.17.1",
+ "eslint-import-resolver-node": "^0.3.9",
+ "get-tsconfig": "^4.7.3",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.3",
+ "semver": "^7.6.3",
+ "stable-hash": "^0.0.4",
+ "tslib": "^2.6.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-import-x/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-import-x/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/eslint-plugin-jest-dom": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-5.5.0.tgz",
+ "integrity": "sha512-CRlXfchTr7EgC3tDI7MGHY6QjdJU5Vv2RPaeeGtkXUHnKZf04kgzMPIJUXt4qKCvYWVVIEo9ut9Oq1vgXAykEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.16.3",
+ "requireindex": "^1.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0",
+ "npm": ">=6",
+ "yarn": ">=1"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^8.0.0 || ^9.0.0 || ^10.0.0",
+ "eslint": "^6.8.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@testing-library/dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react": {
+ "version": "7.37.3",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.3.tgz",
+ "integrity": "sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.8",
+ "array.prototype.findlast": "^1.2.5",
+ "array.prototype.flatmap": "^1.3.3",
+ "array.prototype.tosorted": "^1.1.4",
+ "doctrine": "^2.1.0",
+ "es-iterator-helpers": "^1.2.1",
+ "estraverse": "^5.3.0",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.8",
+ "object.fromentries": "^2.0.8",
+ "object.values": "^1.2.1",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.5",
+ "semver": "^6.3.1",
+ "string.prototype.matchall": "^4.0.12",
+ "string.prototype.repeat": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz",
+ "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/resolve": {
+ "version": "2.0.0-next.5",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
+ "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-testing-library": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-7.1.1.tgz",
+ "integrity": "sha512-nszC833aZPwB6tik1nMkbFqmtgIXTT0sfJEYs0zMBKMlkQ4to2079yUV96SvmLh00ovSBJI4pgcBC1TiIP8mXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "^8.15.0",
+ "@typescript-eslint/utils": "^8.15.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0",
+ "pnpm": "^9.14.0"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
+ "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/eslint/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/eslint/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/espree": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
+ "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/execa": {
+ "version": "9.5.2",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.2.tgz",
+ "integrity": "sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/merge-streams": "^4.0.0",
+ "cross-spawn": "^7.0.3",
+ "figures": "^6.1.0",
+ "get-stream": "^9.0.0",
+ "human-signals": "^8.0.0",
+ "is-plain-obj": "^4.1.0",
+ "is-stream": "^4.0.1",
+ "npm-run-path": "^6.0.0",
+ "pretty-ms": "^9.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-final-newline": "^4.0.0",
+ "yoctocolors": "^2.0.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.5.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/exit-hook": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz",
+ "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz",
+ "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-rate-limit": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
+ "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/express-rate-limit"
+ },
+ "peerDependencies": {
+ "express": "^4.11 || 5 || ^5.0.0-beta.1"
+ }
+ },
+ "node_modules/express/node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+ "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.18.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
+ "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==",
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/figures": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
+ "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-unicode-supported": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "license": "MIT"
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
+ "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/for-each": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+ "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
+ "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.0",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
+ "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/forwarded-parse": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz",
+ "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==",
+ "license": "MIT"
+ },
+ "node_modules/fraction.js": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "license": "MIT"
+ },
+ "node_modules/fs-extra": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
+ "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
+ "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "functions-have-names": "^1.2.3",
+ "hasown": "^2.0.2",
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
+ "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "dunder-proto": "^1.0.0",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "function-bind": "^1.1.2",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/get-port": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz",
+ "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-stdin": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
+ "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
+ "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@sec-ant/readable-stream": "^0.4.1",
+ "is-stream": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz",
+ "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "license": "MIT"
+ },
+ "node_modules/glob": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz",
+ "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==",
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^4.0.1",
+ "minimatch": "^10.0.0",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^2.0.0"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
+ "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/globals": {
+ "version": "15.14.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz",
+ "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/graphql": {
+ "version": "16.10.0",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz",
+ "integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
+ }
+ },
+ "node_modules/gunzip-maybe": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz",
+ "integrity": "sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browserify-zlib": "^0.1.4",
+ "is-deflate": "^1.0.0",
+ "is-gzip": "^1.0.0",
+ "peek-stream": "^1.1.0",
+ "pumpify": "^1.3.3",
+ "through2": "^2.0.3"
+ },
+ "bin": {
+ "gunzip-maybe": "bin.js"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/headers-polyfill": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz",
+ "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/helmet": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.0.0.tgz",
+ "integrity": "sha512-VyusHLEIIO5mjQPUI1wpOAEu+wl6Q0998jzTxqUYGE45xCIcAxy3MsbEK/yyJUJ3ADeMoB6MornPH6GMWAf+Pw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.3.tgz",
+ "integrity": "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^7.5.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/hosted-git-info/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/html-to-text": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
+ "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@selderee/plugin-htmlparser2": "^0.11.0",
+ "deepmerge": "^4.3.1",
+ "dom-serializer": "^2.0.0",
+ "htmlparser2": "^8.0.2",
+ "selderee": "^0.11.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/htmlparser2": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
+ "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1",
+ "entities": "^4.4.0"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/http-proxy-agent/node_modules/agent-base": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
+ "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz",
+ "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-in-the-middle": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.12.0.tgz",
+ "integrity": "sha512-yAgSE7GmtRcu4ZUSFX/4v69UGXwugFFSdIQJ14LHPOPPQrWv8Y7O9PHsw8Ovk7bKCLe4sjXMbZFqGFcLHpZ89w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "acorn": "^8.8.2",
+ "acorn-import-attributes": "^1.9.5",
+ "cjs-module-lexer": "^1.2.2",
+ "module-details-from-path": "^1.0.3"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
+ "node_modules/input-otp": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
+ "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/internal-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/intl-parse-accept-language": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/intl-parse-accept-language/-/intl-parse-accept-language-1.0.0.tgz",
+ "integrity": "sha512-YFMSV91JNBOSjw1cOfw2tup6hDP7mkz+2AUV7W1L1AM6ntgI75qC1ZeFpjPGMrWp+upmBRTX2fJWQ8c7jsUWpA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-async-function": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
+ "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.1.tgz",
+ "integrity": "sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-view": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-deflate": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz",
+ "integrity": "sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-finalizationregistry": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
+ "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-gzip": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz",
+ "integrity": "sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-interactive": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
+ "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-node-process": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
+ "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
+ "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
+ "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.0.tgz",
+ "integrity": "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isbot": {
+ "version": "5.1.22",
+ "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.22.tgz",
+ "integrity": "sha512-RqCFY3cJy3c2y1I+rMn81cfzAR4XJwfPBC+M8kffUjbPzxApzyyv7Tbm1C/gXXq2dSCuD238pKFEWlQMTWsTFw==",
+ "license": "Unlicense",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
+ "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.23",
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
+ "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/iterator.prototype": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz",
+ "integrity": "sha512-x4WH0BWmrMmg4oHHl+duwubhrvczGlyuGAZu3nvrf0UXOfPu8IhZObFEr7DE/iv01YgVZrsOiRcqw2srkKEDIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "has-symbols": "^1.1.0",
+ "reflect.getprototypeof": "^1.0.8",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz",
+ "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "25.0.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz",
+ "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.1.0",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.4.3",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.5",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.12",
+ "parse5": "^7.1.2",
+ "rrweb-cssom": "^0.7.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.0.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^2.11.2"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsdom/node_modules/agent-base": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
+ "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/jsdom/node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
+ "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-parse-better-errors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz",
+ "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsox": {
+ "version": "1.2.121",
+ "resolved": "https://registry.npmjs.org/jsox/-/jsox-1.2.121.tgz",
+ "integrity": "sha512-9Ag50tKhpTwS6r5wh3MJSAvpSof0UBr39Pto8OnzFT32Z/pAbxAsKHzyvsyMEHVslELvHyO/4/jaQELHk8wDcw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsox": "lib/cli.js"
+ }
+ },
+ "node_modules/jsx-ast-utils": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+ "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "object.assign": "^4.1.4",
+ "object.values": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/kleur": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/leac": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
+ "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://ko-fi.com/killymxi"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "license": "MIT"
+ },
+ "node_modules/litefs-js": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/litefs-js/-/litefs-js-1.1.2.tgz",
+ "integrity": "sha512-XKhntdOq8ZXoWbdmANu+PrmJBbf4aIUaHq0356IIk+Uw213evsfg1eNfU0aD04tbF4KXknvG4MWD8sp3D825zg==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^0.5.0"
+ }
+ },
+ "node_modules/litefs-js/node_modules/cookie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/load-json-file": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
+ "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^4.0.0",
+ "pify": "^3.0.0",
+ "strip-bom": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/load-json-file/node_modules/parse-json": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+ "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "error-ex": "^1.3.1",
+ "json-parse-better-errors": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/log-symbols": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz",
+ "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^5.0.0",
+ "is-unicode-supported": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-symbols/node_modules/is-unicode-supported": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
+ "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz",
+ "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "11.0.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz",
+ "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==",
+ "license": "ISC",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.8",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
+ "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.15"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/magicast": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
+ "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.25.4",
+ "@babel/types": "^7.25.4",
+ "source-map-js": "^1.2.0"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/marked": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-7.0.4.tgz",
+ "integrity": "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/md-to-react-email": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/md-to-react-email/-/md-to-react-email-5.0.5.tgz",
+ "integrity": "sha512-OvAXqwq57uOk+WZqFFNCMZz8yDp8BD3WazW1wAKHUrPbbdr89K9DWS6JXY09vd9xNdPNeurI8DU/X4flcfaD8A==",
+ "license": "MIT",
+ "dependencies": {
+ "marked": "7.0.4"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/memorystream": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
+ "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.53.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz",
+ "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
+ "node_modules/module-details-from-path": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz",
+ "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==",
+ "license": "MIT"
+ },
+ "node_modules/moo": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
+ "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/morgan": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
+ "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "basic-auth": "~2.0.1",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-finished": "~2.3.0",
+ "on-headers": "~1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/morgan/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/morgan/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/morgan/node_modules/on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/msw": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/msw/-/msw-2.7.0.tgz",
+ "integrity": "sha512-BIodwZ19RWfCbYTxWTUfTXc+sg4OwjCAgxU1ZsgmggX/7S3LdUifsbUPJs61j0rWb19CZRGY5if77duhc0uXzw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bundled-es-modules/cookie": "^2.0.1",
+ "@bundled-es-modules/statuses": "^1.0.1",
+ "@bundled-es-modules/tough-cookie": "^0.1.6",
+ "@inquirer/confirm": "^5.0.0",
+ "@mswjs/interceptors": "^0.37.0",
+ "@open-draft/deferred-promise": "^2.2.0",
+ "@open-draft/until": "^2.1.0",
+ "@types/cookie": "^0.6.0",
+ "@types/statuses": "^2.0.4",
+ "graphql": "^16.8.1",
+ "headers-polyfill": "^4.0.2",
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.3",
+ "path-to-regexp": "^6.3.0",
+ "picocolors": "^1.1.1",
+ "strict-event-emitter": "^0.5.1",
+ "type-fest": "^4.26.1",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "msw": "cli/index.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mswjs"
+ },
+ "peerDependencies": {
+ "typescript": ">= 4.8.x"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/msw/node_modules/@types/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/msw/node_modules/path-to-regexp": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
+ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mute-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
+ "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/napi-build-utils": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
+ "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==",
+ "license": "MIT"
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nearley": {
+ "version": "2.20.1",
+ "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz",
+ "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^2.19.0",
+ "moo": "^0.5.0",
+ "railroad-diagrams": "^1.0.0",
+ "randexp": "0.4.6"
+ },
+ "bin": {
+ "nearley-railroad": "bin/nearley-railroad.js",
+ "nearley-test": "bin/nearley-test.js",
+ "nearley-unparse": "bin/nearley-unparse.js",
+ "nearleyc": "bin/nearleyc.js"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://nearley.js.org/#give-to-nearley"
+ }
+ },
+ "node_modules/nearley/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-abi": {
+ "version": "3.71.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz",
+ "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/node-fetch/node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-fetch/node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/node-fetch/node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/node-html-parser": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.0.1.tgz",
+ "integrity": "sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-select": "^5.1.0",
+ "he": "1.2.0"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "license": "MIT"
+ },
+ "node_modules/node-sql-parser": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-4.18.0.tgz",
+ "integrity": "sha512-2YEOR5qlI1zUFbGMLKNfsrR5JUvFg9LxIRVE+xJe962pfVLH0rnItqLzv96XVs1Y1UIR8FxsXAuvX/lYAWZ2BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "big-integer": "^1.6.48"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/normalize-package-data": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz",
+ "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^6.0.0",
+ "is-core-module": "^2.8.1",
+ "semver": "^7.3.5",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-install-checks": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz",
+ "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "semver": "^7.1.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-normalize-package-bin": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz",
+ "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-package-arg": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz",
+ "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "hosted-git-info": "^6.0.0",
+ "proc-log": "^3.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-name": "^5.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-pick-manifest": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.2.tgz",
+ "integrity": "sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-install-checks": "^6.0.0",
+ "npm-normalize-package-bin": "^3.0.0",
+ "npm-package-arg": "^10.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-run-all": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
+ "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "chalk": "^2.4.1",
+ "cross-spawn": "^6.0.5",
+ "memorystream": "^0.3.1",
+ "minimatch": "^3.0.4",
+ "pidtree": "^0.3.0",
+ "read-pkg": "^3.0.0",
+ "shell-quote": "^1.6.1",
+ "string.prototype.padend": "^3.0.0"
+ },
+ "bin": {
+ "npm-run-all": "bin/npm-run-all/index.js",
+ "run-p": "bin/run-p/index.js",
+ "run-s": "bin/run-s/index.js"
+ },
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/cross-spawn": {
+ "version": "6.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
+ "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ },
+ "engines": {
+ "node": ">=4.8"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
+ "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^4.0.0",
+ "unicorn-magic": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.16",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.16.tgz",
+ "integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
+ "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz",
+ "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
+ "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
+ "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/ora": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-6.3.1.tgz",
+ "integrity": "sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^5.0.0",
+ "cli-cursor": "^4.0.0",
+ "cli-spinners": "^2.6.1",
+ "is-interactive": "^2.0.0",
+ "is-unicode-supported": "^1.1.0",
+ "log-symbols": "^5.1.0",
+ "stdin-discarder": "^0.1.0",
+ "strip-ansi": "^7.0.1",
+ "wcwidth": "^1.0.1"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora/node_modules/is-unicode-supported": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
+ "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/outvariant": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
+ "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/own-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
+ "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.6",
+ "object-keys": "^1.1.1",
+ "safe-push-apply": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/pako": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
+ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse-json/node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/parse-ms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
+ "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
+ "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^4.5.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parseley": {
+ "version": "0.12.1",
+ "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
+ "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
+ "license": "MIT",
+ "dependencies": {
+ "leac": "^0.6.0",
+ "peberminta": "^0.9.0"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/killymxi"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/path-scurry": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
+ "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
+ "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/peberminta": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
+ "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://ko-fi.com/killymxi"
+ }
+ },
+ "node_modules/peek-stream": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz",
+ "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "duplexify": "^3.5.0",
+ "through2": "^2.0.3"
+ }
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz",
+ "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pidtree": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz",
+ "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
+ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.50.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz",
+ "integrity": "sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.50.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.50.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.1.tgz",
+ "integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/pngjs": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
+ "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
+ "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.49",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+ "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.7",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+ "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+ "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.0.0",
+ "yaml": "^2.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "postcss": ">=8.0.9",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "license": "MIT"
+ },
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
+ "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/prebuild-install": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz",
+ "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^1.0.1",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
+ "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-plugin-sql": {
+ "version": "0.18.1",
+ "resolved": "https://registry.npmjs.org/prettier-plugin-sql/-/prettier-plugin-sql-0.18.1.tgz",
+ "integrity": "sha512-2+Nob2sg7hzLAKJoE6sfgtkhBZCqOzrWHZPvE4Kee/e80oOyI4qwy9vypeltqNBJwTtq3uiKPrCxlT03bBpOaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jsox": "^1.2.119",
+ "node-sql-parser": "^4.12.0",
+ "sql-formatter": "^15.0.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ },
+ "peerDependencies": {
+ "prettier": "^3.0.3"
+ }
+ },
+ "node_modules/prettier-plugin-tailwindcss": {
+ "version": "0.6.11",
+ "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.11.tgz",
+ "integrity": "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.21.3"
+ },
+ "peerDependencies": {
+ "@ianvs/prettier-plugin-sort-imports": "*",
+ "@prettier/plugin-pug": "*",
+ "@shopify/prettier-plugin-liquid": "*",
+ "@trivago/prettier-plugin-sort-imports": "*",
+ "@zackad/prettier-plugin-twig": "*",
+ "prettier": "^3.0",
+ "prettier-plugin-astro": "*",
+ "prettier-plugin-css-order": "*",
+ "prettier-plugin-import-sort": "*",
+ "prettier-plugin-jsdoc": "*",
+ "prettier-plugin-marko": "*",
+ "prettier-plugin-multiline-arrays": "*",
+ "prettier-plugin-organize-attributes": "*",
+ "prettier-plugin-organize-imports": "*",
+ "prettier-plugin-sort-imports": "*",
+ "prettier-plugin-style-order": "*",
+ "prettier-plugin-svelte": "*"
+ },
+ "peerDependenciesMeta": {
+ "@ianvs/prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "@prettier/plugin-pug": {
+ "optional": true
+ },
+ "@shopify/prettier-plugin-liquid": {
+ "optional": true
+ },
+ "@trivago/prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "@zackad/prettier-plugin-twig": {
+ "optional": true
+ },
+ "prettier-plugin-astro": {
+ "optional": true
+ },
+ "prettier-plugin-css-order": {
+ "optional": true
+ },
+ "prettier-plugin-import-sort": {
+ "optional": true
+ },
+ "prettier-plugin-jsdoc": {
+ "optional": true
+ },
+ "prettier-plugin-marko": {
+ "optional": true
+ },
+ "prettier-plugin-multiline-arrays": {
+ "optional": true
+ },
+ "prettier-plugin-organize-attributes": {
+ "optional": true
+ },
+ "prettier-plugin-organize-imports": {
+ "optional": true
+ },
+ "prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "prettier-plugin-style-order": {
+ "optional": true
+ },
+ "prettier-plugin-svelte": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/pretty-format/node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pretty-ms": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz",
+ "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==",
+ "license": "MIT",
+ "dependencies": {
+ "parse-ms": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/prisma": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.3.1.tgz",
+ "integrity": "sha512-JKCZWvBC3enxk51tY4TWzS4b5iRt4sSU1uHn2I183giZTvonXaQonzVtjLzpOHE7qu9MxY510kAtFGJwryKe3Q==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/engines": "6.3.1"
+ },
+ "bin": {
+ "prisma": "build/index.js"
+ },
+ "engines": {
+ "node": ">=18.18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.3"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/prisma/node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/prismjs": {
+ "version": "1.29.0",
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
+ "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/proc-log": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz",
+ "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/promise-inflight": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
+ "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/prompts": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+ "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kleur": "^3.0.3",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/psl": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
+ "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/lupomontero"
+ }
+ },
+ "node_modules/pump": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
+ "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/pumpify": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz",
+ "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "duplexify": "^3.6.0",
+ "inherits": "^2.0.3",
+ "pump": "^2.0.0"
+ }
+ },
+ "node_modules/pumpify/node_modules/pump": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
+ "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qrcode": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
+ "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "dijkstrajs": "^1.0.1",
+ "pngjs": "^5.0.0",
+ "yargs": "^15.3.1"
+ },
+ "bin": {
+ "qrcode": "bin/qrcode"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/qrcode/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/qrcode/node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "node_modules/qrcode/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/qrcode/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/qrcode/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/qrcode/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/qrcode/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+ "license": "ISC"
+ },
+ "node_modules/qrcode/node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/querystringify": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/railroad-diagrams": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
+ "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/randexp": {
+ "version": "0.4.6",
+ "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
+ "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "discontinuous-range": "1.0.0",
+ "ret": "~0.1.10"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/rc/node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
+ "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
+ "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.25.0"
+ },
+ "peerDependencies": {
+ "react": "^19.0.0"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/react-promise-suspense": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz",
+ "integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^2.0.1"
+ }
+ },
+ "node_modules/react-promise-suspense/node_modules/fast-deep-equal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+ "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==",
+ "license": "MIT"
+ },
+ "node_modules/react-refresh": {
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
+ "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz",
+ "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.5.tgz",
+ "integrity": "sha512-8BUF+hZEU4/z/JD201yK6S+UYhsf58bzYIDq2NS1iGpwxSXDu7F+DeGSkIXMFBuHZB21FSiCzEcUb18cQNdRkA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/cookie": "^0.6.0",
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0",
+ "turbo-stream": "2.4.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router/node_modules/@types/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
+ "license": "MIT"
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/read-cache/node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/read-pkg": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
+ "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "load-json-file": "^4.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/read-pkg/node_modules/hosted-git-info": {
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/read-pkg/node_modules/normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "node_modules/read-pkg/node_modules/path-type": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+ "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/read-pkg/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/readable-stream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/readdirp": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
+ "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.9.tgz",
+ "integrity": "sha512-r0Ay04Snci87djAsI4U+WNRcSw5S4pOH7qFjd/veA5gC7TbqESR3tcj28ia95L/fYUDw11JKP7uqUKUAfVvV5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "dunder-proto": "^1.0.1",
+ "es-abstract": "^1.23.6",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "gopd": "^1.2.0",
+ "which-builtin-type": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz",
+ "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/remix-auth": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/remix-auth/-/remix-auth-3.7.0.tgz",
+ "integrity": "sha512-2QVjp2nJVaYxuFBecMQwzixCO7CLSssttLBU5eVlNcNlVeNMmY1g7OkmZ1Ogw9sBcoMXZ18J7xXSK0AISVFcfQ==",
+ "license": "MIT",
+ "dependencies": {
+ "uuid": "^8.3.2"
+ },
+ "peerDependencies": {
+ "@remix-run/react": "^1.0.0 || ^2.0.0",
+ "@remix-run/server-runtime": "^1.0.0 || ^2.0.0"
+ }
+ },
+ "node_modules/remix-auth-github": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/remix-auth-github/-/remix-auth-github-1.7.0.tgz",
+ "integrity": "sha512-xuy/DW44y/eWU+vUsf9NlQUtLhayMZEJszgaVr1txwjA0OzpPee5qNxPQ9RBg8WdnY3pMWck5m070MW5Jt7nxg==",
+ "license": "MIT",
+ "dependencies": {
+ "remix-auth-oauth2": "^1.11.2"
+ },
+ "peerDependencies": {
+ "@remix-run/server-runtime": "^1.0.0 || ^2.0.0",
+ "remix-auth": "^3.4.0"
+ }
+ },
+ "node_modules/remix-auth-oauth2": {
+ "version": "1.11.2",
+ "resolved": "https://registry.npmjs.org/remix-auth-oauth2/-/remix-auth-oauth2-1.11.2.tgz",
+ "integrity": "sha512-5ORP+LMi5CVCA/Wb8Z+FCAJ73Uiy4uyjEzhlVwNBfdAkPOnfxzoi+q/pY/CrueYv3OniCXRM35ZYqkVi3G1UPw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.4",
+ "uuid": "^9.0.1"
+ },
+ "peerDependencies": {
+ "@remix-run/server-runtime": "^1.0.0 || ^2.0.0",
+ "remix-auth": "^3.6.0"
+ }
+ },
+ "node_modules/remix-auth-oauth2/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/remix-flat-routes": {
+ "version": "0.8.4",
+ "resolved": "https://registry.npmjs.org/remix-flat-routes/-/remix-flat-routes-0.8.4.tgz",
+ "integrity": "sha512-W+kwaICoFwzKS14j3bixoOGlQ2WZ3PNo18QGADpBGxjcXDfZAZ6OnHJT8SU2Q+/r83YPGvu4yijjIRlBcfokxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fs-extra": "^11.2.0",
+ "minimatch": "^10.0.1"
+ },
+ "bin": {
+ "migrate-flat-routes": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=16.6.0"
+ },
+ "peerDependencies": {
+ "react-router": "7"
+ },
+ "peerDependenciesMeta": {
+ "react-router": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/remix-flat-routes/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/remix-flat-routes/node_modules/minimatch": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
+ "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/remix-utils": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/remix-utils/-/remix-utils-8.1.0.tgz",
+ "integrity": "sha512-k44K4FOPyMv6QQ+yaQrCrXdnzkKE1b86ZUODAK5fvmWsmi7ntZAAA+Ods+nzNAxFEAPDTreCzl7cGWnxWd6ibw==",
+ "funding": [
+ "https://github.com/sponsors/sergiodxa"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^4.33.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@oslojs/crypto": "^1.0.1",
+ "@oslojs/encoding": "^1.1.0",
+ "intl-parse-accept-language": "^1.0.0",
+ "is-ip": "^5.0.1",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-router": "7.0.0",
+ "zod": "^3.22.4"
+ },
+ "peerDependenciesMeta": {
+ "@oslojs/crypto": {
+ "optional": true
+ },
+ "@oslojs/encoding": {
+ "optional": true
+ },
+ "intl-parse-accept-language": {
+ "optional": true
+ },
+ "is-ip": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-router": {
+ "optional": true
+ },
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-in-the-middle": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.4.0.tgz",
+ "integrity": "sha512-X34iHADNbNDfr6OTStIAHWSAvvKQRYgLO6duASaVf7J2VA3lvmNYboAHOuLC2huav1IwgZJtyEcJCKVzFxOSMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.5",
+ "module-details-from-path": "^1.0.3",
+ "resolve": "^1.22.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "license": "ISC"
+ },
+ "node_modules/requireindex": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
+ "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.5"
+ }
+ },
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/resolve": {
+ "version": "1.22.10",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+ "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
+ "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ret": {
+ "version": "0.1.15",
+ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.1.tgz",
+ "integrity": "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.6"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.29.1",
+ "@rollup/rollup-android-arm64": "4.29.1",
+ "@rollup/rollup-darwin-arm64": "4.29.1",
+ "@rollup/rollup-darwin-x64": "4.29.1",
+ "@rollup/rollup-freebsd-arm64": "4.29.1",
+ "@rollup/rollup-freebsd-x64": "4.29.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.29.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.29.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.29.1",
+ "@rollup/rollup-linux-arm64-musl": "4.29.1",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.29.1",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.29.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.29.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.29.1",
+ "@rollup/rollup-linux-x64-gnu": "4.29.1",
+ "@rollup/rollup-linux-x64-musl": "4.29.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.29.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.29.1",
+ "@rollup/rollup-win32-x64-msvc": "4.29.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rrweb-cssom": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
+ "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
+ "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "has-symbols": "^1.1.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-array-concat/node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safe-push-apply": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+ "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-push-apply/node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
+ "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
+ "license": "MIT"
+ },
+ "node_modules/selderee": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
+ "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
+ "license": "MIT",
+ "dependencies": {
+ "parseley": "^0.12.0"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/killymxi"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "license": "ISC"
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
+ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
+ "license": "MIT"
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
+ "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/shimmer": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
+ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "node_modules/sisteransi": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/sonner": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz",
+ "integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
+ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/spdx-correct": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+ "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+ "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+ "dev": true,
+ "license": "CC-BY-3.0"
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.20",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz",
+ "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/spin-delay": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/spin-delay/-/spin-delay-2.0.1.tgz",
+ "integrity": "sha512-ilggKXKqAMwk21PSYvxuF/KCnrsGFDrnO6mXa629mj8fvfo+dOQfubDViqsRjRX5U1jd3Xb8FTsV+m4Tg7YeUg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=17.0.1"
+ }
+ },
+ "node_modules/sql-formatter": {
+ "version": "15.4.8",
+ "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.4.8.tgz",
+ "integrity": "sha512-DNLFVjKB6QaBli00LaNEJgLeBIPygD6L35hKPbOVi01hK+7sTpShOc2+pa6FgDobcpHKI6+FKswkp7PiNiDaTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "get-stdin": "=8.0.0",
+ "nearley": "^2.20.1"
+ },
+ "bin": {
+ "sql-formatter": "bin/sql-formatter-cli.cjs"
+ }
+ },
+ "node_modules/stable-hash": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz",
+ "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/std-env": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz",
+ "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stdin-discarder": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz",
+ "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^5.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/stream-shift": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
+ "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stream-slice": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/stream-slice/-/stream-slice-0.1.2.tgz",
+ "integrity": "sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==",
+ "license": "MIT"
+ },
+ "node_modules/strict-event-emitter": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
+ "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/string_decoder/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string.prototype.matchall": {
+ "version": "4.0.12",
+ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
+ "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.6",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "regexp.prototype.flags": "^1.5.3",
+ "set-function-name": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.padend": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz",
+ "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.repeat": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
+ "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
+ "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-data-property": "^1.1.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-object-atoms": "^1.0.0",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
+ "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
+ "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.0",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
+ "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "glob": "^10.3.10",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/sucrase/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/sucrase/node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/sucrase/node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/sucrase/node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/sucrase/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/sucrase/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/sucrase/node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tailwind-merge": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
+ "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.17",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
+ "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.6",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
+ },
+ "node_modules/tailwindcss-radix": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/tailwindcss-radix/-/tailwindcss-radix-3.0.5.tgz",
+ "integrity": "sha512-dy5bIHixuvrmMzljDoteKD1mcLAm76Z1IPkCr7IWUN89zoxfokVepPlxMQkmWEQokYs3N9BOLsXXvn8fLZWoBg==",
+ "license": "MIT",
+ "engines": {
+ "pnpm": "9"
+ },
+ "peerDependencies": {
+ "tailwindcss": "^3.4.1"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/tapable": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
+ "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar-stream/node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/tar-stream/node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/tar-stream/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/test-exclude": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
+ "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^10.4.1",
+ "minimatch": "^9.0.4"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/test-exclude/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/test-exclude/node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/test-exclude/node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/test-exclude/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/test-exclude/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/test-exclude/node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinypool": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz",
+ "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
+ "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "6.1.70",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.70.tgz",
+ "integrity": "sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.70"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.70",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.70.tgz",
+ "integrity": "sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/to-data-view": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-2.0.0.tgz",
+ "integrity": "sha512-RGEM5KqlPHr+WVTPmGNAXNeFEmsBnlkxXaIfEpUYV0AST2Z5W1EGq9L/MENFrMMmL2WQr1wjkmZy/M92eKhjYA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz",
+ "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
+ "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
+ "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/tsx": {
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz",
+ "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.23.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
+ "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz",
+ "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz",
+ "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-x64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz",
+ "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz",
+ "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz",
+ "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz",
+ "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz",
+ "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz",
+ "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz",
+ "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz",
+ "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz",
+ "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz",
+ "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz",
+ "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz",
+ "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz",
+ "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-x64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz",
+ "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz",
+ "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz",
+ "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz",
+ "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz",
+ "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz",
+ "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz",
+ "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-x64": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz",
+ "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/esbuild": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz",
+ "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.23.1",
+ "@esbuild/android-arm": "0.23.1",
+ "@esbuild/android-arm64": "0.23.1",
+ "@esbuild/android-x64": "0.23.1",
+ "@esbuild/darwin-arm64": "0.23.1",
+ "@esbuild/darwin-x64": "0.23.1",
+ "@esbuild/freebsd-arm64": "0.23.1",
+ "@esbuild/freebsd-x64": "0.23.1",
+ "@esbuild/linux-arm": "0.23.1",
+ "@esbuild/linux-arm64": "0.23.1",
+ "@esbuild/linux-ia32": "0.23.1",
+ "@esbuild/linux-loong64": "0.23.1",
+ "@esbuild/linux-mips64el": "0.23.1",
+ "@esbuild/linux-ppc64": "0.23.1",
+ "@esbuild/linux-riscv64": "0.23.1",
+ "@esbuild/linux-s390x": "0.23.1",
+ "@esbuild/linux-x64": "0.23.1",
+ "@esbuild/netbsd-x64": "0.23.1",
+ "@esbuild/openbsd-arm64": "0.23.1",
+ "@esbuild/openbsd-x64": "0.23.1",
+ "@esbuild/sunos-x64": "0.23.1",
+ "@esbuild/win32-arm64": "0.23.1",
+ "@esbuild/win32-ia32": "0.23.1",
+ "@esbuild/win32-x64": "0.23.1"
+ }
+ },
+ "node_modules/tsx/node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/turbo-stream": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
+ "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
+ "license": "ISC"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.33.0.tgz",
+ "integrity": "sha512-s6zVrxuyKbbAsSAD5ZPTB77q4YIdRctkTbJ2/Dqlinwz+8ooH2gd+YA7VA6Pa93KML9GockVvoxjZ2vHP+mu8g==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+ "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+ "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "reflect.getprototypeof": "^1.0.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz",
+ "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "is-typed-array": "^1.1.13",
+ "possible-typed-array-names": "^1.0.0",
+ "reflect.getprototypeof": "^1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.7.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
+ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.19.0.tgz",
+ "integrity": "sha512-Ni8sUkVWYK4KAcTtPjQ/UTiRk6jcsuDhPpxULapUDi8A/l8TSBk+t1GtJA1RsCzIJg0q6+J7bf35AwQigENWRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.19.0",
+ "@typescript-eslint/parser": "8.19.0",
+ "@typescript-eslint/utils": "8.19.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.8.0"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+ "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "which-boxed-primitive": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/undici": {
+ "version": "6.21.1",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
+ "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+ "license": "MIT"
+ },
+ "node_modules/unicorn-magic": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
+ "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/unplugin": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz",
+ "integrity": "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.8.1",
+ "chokidar": "^3.5.3",
+ "webpack-sources": "^3.2.3",
+ "webpack-virtual-modules": "^0.5.0"
+ }
+ },
+ "node_modules/unplugin/node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/unplugin/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/unplugin/node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
+ "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.0"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/url-parse": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
+ "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "querystringify": "^2.1.1",
+ "requires-port": "^1.0.0"
+ }
+ },
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/valibot": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.41.0.tgz",
+ "integrity": "sha512-igDBb8CTYr8YTQlOKgaN9nSS0Be7z+WRuaeYqGf3Cjz3aKmSnqEmYnkfVjzIuumGqfHpa3fLIvMEAfhrpqN8ng==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "typescript": ">=5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/validate-npm-package-name": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz",
+ "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.0.11",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz",
+ "integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.24.2",
+ "postcss": "^8.4.49",
+ "rollup": "^4.23.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-env-only": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/vite-env-only/-/vite-env-only-3.0.3.tgz",
+ "integrity": "sha512-iAb7cTXRrvFShaF1n+G8f6Yqq7sRJcxipNYNQQu0DN5N9P55vJMmLG5lNU5moYGpd+ZH1WhBHdkWi5WjrfImHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.23.7",
+ "@babel/generator": "^7.23.6",
+ "@babel/parser": "^7.23.6",
+ "@babel/traverse": "^7.23.7",
+ "@babel/types": "^7.23.6",
+ "babel-dead-code-elimination": "^1.0.6",
+ "micromatch": "^4.0.5"
+ },
+ "peerDependencies": {
+ "vite": "*"
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "3.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.0-beta.2.tgz",
+ "integrity": "sha512-ofTf6cfRdL30Wbl9n/BX81EyIR5s4PReLmSurrxQ+koLaWUNOEo8E0lCM53OJkb8vpa2URM2nSrxZsIFyvY1rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.0",
+ "es-module-lexer": "^1.5.4",
+ "pathe": "^1.1.2",
+ "vite": "^5.0.0 || ^6.0.0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vite/node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz",
+ "integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "3.0.5",
+ "@vitest/mocker": "3.0.5",
+ "@vitest/pretty-format": "^3.0.5",
+ "@vitest/runner": "3.0.5",
+ "@vitest/snapshot": "3.0.5",
+ "@vitest/spy": "3.0.5",
+ "@vitest/utils": "3.0.5",
+ "chai": "^5.1.2",
+ "debug": "^4.4.0",
+ "expect-type": "^1.1.0",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.2",
+ "std-env": "^3.8.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinypool": "^1.0.2",
+ "tinyrainbow": "^2.0.0",
+ "vite": "^5.0.0 || ^6.0.0",
+ "vite-node": "3.0.5",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/debug": "^4.1.12",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@vitest/browser": "3.0.5",
+ "@vitest/ui": "3.0.5",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/debug": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/magic-string": {
+ "version": "0.30.17",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
+ "node_modules/vitest/node_modules/pathe": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz",
+ "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vitest/node_modules/vite-node": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz",
+ "integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.0",
+ "es-module-lexer": "^1.6.0",
+ "pathe": "^2.0.2",
+ "vite": "^5.0.0 || ^6.0.0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wcwidth": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+ "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "defaults": "^1.0.3"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/webpack-sources": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
+ "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/webpack-virtual-modules": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
+ "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-encoding/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz",
+ "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.0.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/which": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz",
+ "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.1.0",
+ "is-boolean-object": "^1.2.1",
+ "is-number-object": "^1.1.1",
+ "is-string": "^1.1.1",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+ "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "function.prototype.name": "^1.1.6",
+ "has-tostringtag": "^1.0.2",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.1.0",
+ "is-finalizationregistry": "^1.1.0",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.2.1",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.1.0",
+ "which-collection": "^1.0.2",
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type/node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-module": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
+ "license": "ISC"
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.18",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz",
+ "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/ws": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "license": "ISC"
+ },
+ "node_modules/yaml": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
+ "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yargs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yoctocolors": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz",
+ "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yoctocolors-cjs": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz",
+ "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.24.1",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
+ "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/examples/federation/epic-stack-remote/package.json b/examples/federation/epic-stack-remote/package.json
new file mode 100644
index 0000000..35926da
--- /dev/null
+++ b/examples/federation/epic-stack-remote/package.json
@@ -0,0 +1,174 @@
+{
+ "name": "epic-stack-remote",
+ "private": true,
+ "license": "MIT",
+ "author": "Kent C. Dodds (https://kentcdodds.com/)",
+ "sideEffects": false,
+ "type": "module",
+ "imports": {
+ "#app/*": "./app/*",
+ "#tests/*": "./tests/*"
+ },
+ "scripts": {
+ "build": "run-s build:*",
+ "build:icons": "tsx ./other/build-icons.ts",
+ "build:remix": "rsbuild build",
+ "build:server": "tsx ./other/build-server.ts",
+ "predev": "npm run build:icons --silent",
+ "dev": "cross-env NODE_ENV=development MOCKS=true PORT=3001 NODE_OPTIONS=--experimental-vm-modules tsx ./server/dev-server.js",
+ "prisma:studio": "prisma studio",
+ "format": "prettier --write .",
+ "lint": "eslint .",
+ "setup": "prisma generate && prisma migrate reset --force && playwright install && pnpm run build",
+ "start": "cross-env NODE_ENV=production NODE_OPTIONS=--experimental-vm-modules node .",
+ "start:mocks": "cross-env NODE_ENV=production MOCKS=true NODE_OPTIONS=--experimental-vm-modules tsx .",
+ "test": "vitest",
+ "coverage": "vitest run --coverage",
+ "test:e2e": "npm run test:e2e:dev --silent",
+ "test:e2e:dev": "playwright test --ui",
+ "pretest:e2e:run": "npm run build",
+ "test:e2e:run": "cross-env CI=true playwright test",
+ "test:e2e:install": "npx playwright install --with-deps chromium",
+ "typecheck": "react-router typegen && tsc",
+ "validate": "run-p \"test -- --run\" lint typecheck test:e2e:run"
+ },
+ "prettier": "@epic-web/config/prettier",
+ "eslintIgnore": [
+ "/node_modules",
+ "/build",
+ "/public/build",
+ "/playwright-report",
+ "/server-build"
+ ],
+ "dependencies": {
+ "@conform-to/react": "1.2.2",
+ "@conform-to/zod": "1.2.2",
+ "@epic-web/cachified": "5.2.0",
+ "@epic-web/client-hints": "1.3.5",
+ "@epic-web/invariant": "1.0.0",
+ "@epic-web/remember": "1.1.0",
+ "@epic-web/totp": "2.1.1",
+ "@mjackson/form-data-parser": "0.7.0",
+ "@module-federation/enhanced": "0.0.0-next-20250321011937",
+ "@module-federation/node": "0.0.0-next-20250321011937",
+ "@module-federation/rsbuild-plugin": "0.0.0-next-20250321011937",
+ "@nasa-gcn/remix-seo": "2.0.1",
+ "@oslojs/crypto": "1.0.1",
+ "@oslojs/encoding": "1.1.0",
+ "@paralleldrive/cuid2": "2.2.2",
+ "@prisma/client": "6.3.1",
+ "@prisma/instrumentation": "6.3.1",
+ "@radix-ui/react-checkbox": "1.1.3",
+ "@radix-ui/react-dropdown-menu": "2.1.5",
+ "@radix-ui/react-label": "2.1.1",
+ "@radix-ui/react-slot": "1.1.1",
+ "@radix-ui/react-toast": "1.2.5",
+ "@radix-ui/react-tooltip": "1.1.7",
+ "@react-email/components": "0.0.32",
+ "@react-router/express": "7.4.0",
+ "@react-router/node": "^7.4.0",
+ "@react-router/remix-routes-option-adapter": "7.4.0",
+ "@remix-run/server-runtime": "2.15.3",
+ "@rsbuild/core": "1.2.19",
+ "@rsbuild/plugin-react": "1.1.1",
+ "@rsbuild/plugin-react-router": "workspace:*",
+ "@sentry/node": "8.54.0",
+ "@sentry/profiling-node": "8.54.0",
+ "@sentry/react": "8.54.0",
+ "@tusbar/cache-control": "1.0.2",
+ "address": "2.0.3",
+ "bcryptjs": "2.4.3",
+ "better-sqlite3": "11.8.1",
+ "chalk": "5.4.1",
+ "class-variance-authority": "0.7.1",
+ "close-with-grace": "2.2.0",
+ "clsx": "2.1.1",
+ "compression": "1.7.5",
+ "cookie": "1.0.2",
+ "cross-env": "7.0.3",
+ "date-fns": "4.1.0",
+ "dotenv": "16.4.7",
+ "execa": "9.5.2",
+ "express": "4.21.2",
+ "express-rate-limit": "7.5.0",
+ "get-port": "7.1.0",
+ "glob": "11.0.1",
+ "helmet": "8.0.0",
+ "input-otp": "1.4.2",
+ "intl-parse-accept-language": "1.0.0",
+ "isbot": "5.1.22",
+ "litefs-js": "1.1.2",
+ "lru-cache": "11.0.2",
+ "morgan": "1.10.0",
+ "prisma": "6.3.1",
+ "qrcode": "1.5.4",
+ "react": "19.0.0",
+ "react-dom": "19.0.0",
+ "react-router": "^7.4.0",
+ "remix-auth": "3.7.0",
+ "remix-auth-github": "1.7.0",
+ "remix-utils": "8.1.0",
+ "set-cookie-parser": "2.7.1",
+ "sonner": "1.7.4",
+ "source-map-support": "0.5.21",
+ "spin-delay": "2.0.1",
+ "tailwind-merge": "2.6.0",
+ "tailwindcss": "3.4.17",
+ "tailwindcss-animate": "1.0.7",
+ "tailwindcss-radix": "3.0.5",
+ "zod": "3.24.1"
+ },
+ "devDependencies": {
+ "@epic-web/config": "1.16.5",
+ "@faker-js/faker": "9.4.0",
+ "@playwright/test": "1.50.1",
+ "@react-router/dev": "^7.4.0",
+ "@sly-cli/sly": "1.14.0",
+ "@testing-library/dom": "10.4.0",
+ "@testing-library/jest-dom": "6.6.3",
+ "@testing-library/react": "16.2.0",
+ "@testing-library/user-event": "14.6.1",
+ "@total-typescript/ts-reset": "0.6.1",
+ "@types/bcryptjs": "2.4.6",
+ "@types/better-sqlite3": "7.6.12",
+ "@types/compression": "1.7.5",
+ "@types/eslint": "9.6.1",
+ "@types/express": "5.0.0",
+ "@types/fs-extra": "11.0.4",
+ "@types/glob": "8.1.0",
+ "@types/morgan": "1.9.9",
+ "@types/node": "22.13.1",
+ "@types/qrcode": "1.5.5",
+ "@types/react": "19.0.8",
+ "@types/react-dom": "19.0.3",
+ "@types/set-cookie-parser": "2.4.10",
+ "@types/source-map-support": "0.5.10",
+ "@vitest/coverage-v8": "3.0.5",
+ "autoprefixer": "10.4.20",
+ "enforce-unique": "1.3.0",
+ "esbuild": "0.24.2",
+ "eslint": "9.19.0",
+ "fs-extra": "11.3.0",
+ "jsdom": "25.0.1",
+ "msw": "2.7.0",
+ "node-html-parser": "7.0.1",
+ "npm-run-all": "4.1.5",
+ "prettier": "3.4.2",
+ "prettier-plugin-sql": "0.18.1",
+ "prettier-plugin-tailwindcss": "0.6.11",
+ "remix-flat-routes": "0.8.4",
+ "tsx": "4.19.2",
+ "typescript": "5.7.3",
+ "vitest": "3.0.5"
+ },
+ "engines": {
+ "node": "22"
+ },
+ "epic-stack": {
+ "head": "80b114fc0c1f3d6402627bc030d82e205dc3293c",
+ "date": "2025-02-11T04:40:56Z"
+ },
+ "prisma": {
+ "seed": "tsx prisma/seed.ts"
+ }
+}
diff --git a/examples/federation/epic-stack-remote/playwright.config.ts b/examples/federation/epic-stack-remote/playwright.config.ts
new file mode 100644
index 0000000..d1b443a
--- /dev/null
+++ b/examples/federation/epic-stack-remote/playwright.config.ts
@@ -0,0 +1,42 @@
+import { defineConfig, devices } from '@playwright/test'
+import 'dotenv/config'
+
+const PORT = process.env.PORT || '3001'
+
+export default defineConfig({
+ testDir: './tests/e2e',
+ timeout: 15 * 1000,
+ expect: {
+ timeout: 5 * 1000,
+ },
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: process.env.CI ? 1 : undefined,
+ reporter: 'html',
+ use: {
+ baseURL: `http://localhost:${PORT}/`,
+ trace: 'on-first-retry',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: {
+ ...devices['Desktop Chrome'],
+ },
+ },
+ ],
+
+ webServer: {
+ command: process.env.CI ? 'npm run start:mocks' : 'npm run dev',
+ port: Number(PORT),
+ reuseExistingServer: true,
+ stdout: 'pipe',
+ stderr: 'pipe',
+ env: {
+ PORT,
+ NODE_ENV: 'test',
+ },
+ },
+})
diff --git a/examples/federation/epic-stack-remote/postcss.config.js b/examples/federation/epic-stack-remote/postcss.config.js
new file mode 100644
index 0000000..5ebad51
--- /dev/null
+++ b/examples/federation/epic-stack-remote/postcss.config.js
@@ -0,0 +1,7 @@
+export default {
+ plugins: {
+ 'tailwindcss/nesting': {},
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/examples/federation/epic-stack-remote/prisma/migrations/20230914194400_init/migration.sql b/examples/federation/epic-stack-remote/prisma/migrations/20230914194400_init/migration.sql
new file mode 100644
index 0000000..5ee0147
--- /dev/null
+++ b/examples/federation/epic-stack-remote/prisma/migrations/20230914194400_init/migration.sql
@@ -0,0 +1,258 @@
+-- CreateTable
+CREATE TABLE "User" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "email" TEXT NOT NULL,
+ "username" TEXT NOT NULL,
+ "name" TEXT,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL
+);
+
+-- CreateTable
+CREATE TABLE "Note" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "title" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL,
+ "ownerId" TEXT NOT NULL,
+ CONSTRAINT "Note_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "NoteImage" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "altText" TEXT,
+ "contentType" TEXT NOT NULL,
+ "blob" BLOB NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL,
+ "noteId" TEXT NOT NULL,
+ CONSTRAINT "NoteImage_noteId_fkey" FOREIGN KEY ("noteId") REFERENCES "Note" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "UserImage" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "altText" TEXT,
+ "contentType" TEXT NOT NULL,
+ "blob" BLOB NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL,
+ "userId" TEXT NOT NULL,
+ CONSTRAINT "UserImage_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "Password" (
+ "hash" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ CONSTRAINT "Password_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "Session" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "expirationDate" DATETIME NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL,
+ "userId" TEXT NOT NULL,
+ CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "Permission" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "action" TEXT NOT NULL,
+ "entity" TEXT NOT NULL,
+ "access" TEXT NOT NULL,
+ "description" TEXT NOT NULL DEFAULT '',
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL
+);
+
+-- CreateTable
+CREATE TABLE "Role" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "name" TEXT NOT NULL,
+ "description" TEXT NOT NULL DEFAULT '',
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL
+);
+
+-- CreateTable
+CREATE TABLE "Verification" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "type" TEXT NOT NULL,
+ "target" TEXT NOT NULL,
+ "secret" TEXT NOT NULL,
+ "algorithm" TEXT NOT NULL,
+ "digits" INTEGER NOT NULL,
+ "period" INTEGER NOT NULL,
+ "charSet" TEXT NOT NULL,
+ "expiresAt" DATETIME
+);
+
+-- CreateTable
+CREATE TABLE "Connection" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "providerName" TEXT NOT NULL,
+ "providerId" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL,
+ "userId" TEXT NOT NULL,
+ CONSTRAINT "Connection_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "_PermissionToRole" (
+ "A" TEXT NOT NULL,
+ "B" TEXT NOT NULL,
+ CONSTRAINT "_PermissionToRole_A_fkey" FOREIGN KEY ("A") REFERENCES "Permission" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "_PermissionToRole_B_fkey" FOREIGN KEY ("B") REFERENCES "Role" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "_RoleToUser" (
+ "A" TEXT NOT NULL,
+ "B" TEXT NOT NULL,
+ CONSTRAINT "_RoleToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Role" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "_RoleToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
+
+-- CreateIndex
+CREATE INDEX "Note_ownerId_idx" ON "Note"("ownerId");
+
+-- CreateIndex
+CREATE INDEX "Note_ownerId_updatedAt_idx" ON "Note"("ownerId", "updatedAt");
+
+-- CreateIndex
+CREATE INDEX "NoteImage_noteId_idx" ON "NoteImage"("noteId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "UserImage_userId_key" ON "UserImage"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Password_userId_key" ON "Password"("userId");
+
+-- CreateIndex
+CREATE INDEX "Session_userId_idx" ON "Session"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Permission_action_entity_access_key" ON "Permission"("action", "entity", "access");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Role_name_key" ON "Role"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Verification_target_type_key" ON "Verification"("target", "type");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Connection_providerName_providerId_key" ON "Connection"("providerName", "providerId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "_PermissionToRole_AB_unique" ON "_PermissionToRole"("A", "B");
+
+-- CreateIndex
+CREATE INDEX "_PermissionToRole_B_index" ON "_PermissionToRole"("B");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "_RoleToUser_AB_unique" ON "_RoleToUser"("A", "B");
+
+-- CreateIndex
+CREATE INDEX "_RoleToUser_B_index" ON "_RoleToUser"("B");
+
+--------------------------------- Manual Seeding --------------------------
+-- Hey there, Kent here! This is how you can reliably seed your database with
+-- some data. You edit the migration.sql file and that will handle it for you.
+
+-- The user Roles and Permissions are seeded here.
+-- If you'd like to customise roles and permissions, you can edit and add the code below to your `prisma/seed.ts` file.
+-- Seed your development database with `npx prisma db seed`
+-- Create a sql dump of your database with `sqlite3 prisma/data.db .dump > seed.sql`
+-- Replace the SQL below with your new Roles & Permissions related SQL from `seed.sql`
+
+-- console.time('π Created permissions...')
+-- const entities = ['user', 'note']
+-- const actions = ['create', 'read', 'update', 'delete']
+-- const accesses = ['own', 'any'] as const
+
+-- let permissionsToCreate = []
+-- for (const entity of entities) {
+-- for (const action of actions) {
+-- for (const access of accesses) {
+-- permissionsToCreate.push({ entity, action, access })
+-- }
+-- }
+-- }
+-- await prisma.permission.createMany({ data: permissionsToCreate })
+-- console.timeEnd('π Created permissions...')
+
+-- console.time('π Created roles...')
+-- await prisma.role.create({
+-- data: {
+-- name: 'admin',
+-- permissions: {
+-- connect: await prisma.permission.findMany({
+-- select: { id: true },
+-- where: { access: 'any' },
+-- }),
+-- },
+-- },
+-- })
+-- await prisma.role.create({
+-- data: {
+-- name: 'user',
+-- permissions: {
+-- connect: await prisma.permission.findMany({
+-- select: { id: true },
+-- where: { access: 'own' },
+-- }),
+-- },
+-- },
+-- })
+-- console.timeEnd('π Created roles...')
+
+INSERT INTO Permission VALUES('clnf2zvli0000pcou3zzzzome','create','user','own','',1696625465526,1696625465526);
+INSERT INTO Permission VALUES('clnf2zvll0001pcouly1310ku','create','user','any','',1696625465529,1696625465529);
+INSERT INTO Permission VALUES('clnf2zvll0002pcouka7348re','read','user','own','',1696625465530,1696625465530);
+INSERT INTO Permission VALUES('clnf2zvlm0003pcouea4dee51','read','user','any','',1696625465530,1696625465530);
+INSERT INTO Permission VALUES('clnf2zvlm0004pcou2guvolx5','update','user','own','',1696625465531,1696625465531);
+INSERT INTO Permission VALUES('clnf2zvln0005pcoun78ps5ap','update','user','any','',1696625465531,1696625465531);
+INSERT INTO Permission VALUES('clnf2zvlo0006pcouyoptc5jp','delete','user','own','',1696625465532,1696625465532);
+INSERT INTO Permission VALUES('clnf2zvlo0007pcouw1yzoyam','delete','user','any','',1696625465533,1696625465533);
+INSERT INTO Permission VALUES('clnf2zvlp0008pcou9r0fhbm8','create','note','own','',1696625465533,1696625465533);
+INSERT INTO Permission VALUES('clnf2zvlp0009pcouj3qib9q9','create','note','any','',1696625465534,1696625465534);
+INSERT INTO Permission VALUES('clnf2zvlq000apcouxnspejs9','read','note','own','',1696625465535,1696625465535);
+INSERT INTO Permission VALUES('clnf2zvlr000bpcouf4cg3x72','read','note','any','',1696625465535,1696625465535);
+INSERT INTO Permission VALUES('clnf2zvlr000cpcouy1vp6oeg','update','note','own','',1696625465536,1696625465536);
+INSERT INTO Permission VALUES('clnf2zvls000dpcouvzwjjzrq','update','note','any','',1696625465536,1696625465536);
+INSERT INTO Permission VALUES('clnf2zvls000epcou4ts5ui8f','delete','note','own','',1696625465537,1696625465537);
+INSERT INTO Permission VALUES('clnf2zvlt000fpcouk29jbmxn','delete','note','any','',1696625465538,1696625465538);
+
+INSERT INTO Role VALUES('clnf2zvlw000gpcour6dyyuh6','admin','',1696625465540,1696625465540);
+INSERT INTO Role VALUES('clnf2zvlx000hpcou5dfrbegs','user','',1696625465542,1696625465542);
+
+INSERT INTO _PermissionToRole VALUES('clnf2zvll0001pcouly1310ku','clnf2zvlw000gpcour6dyyuh6');
+INSERT INTO _PermissionToRole VALUES('clnf2zvlm0003pcouea4dee51','clnf2zvlw000gpcour6dyyuh6');
+INSERT INTO _PermissionToRole VALUES('clnf2zvln0005pcoun78ps5ap','clnf2zvlw000gpcour6dyyuh6');
+INSERT INTO _PermissionToRole VALUES('clnf2zvlo0007pcouw1yzoyam','clnf2zvlw000gpcour6dyyuh6');
+INSERT INTO _PermissionToRole VALUES('clnf2zvlp0009pcouj3qib9q9','clnf2zvlw000gpcour6dyyuh6');
+INSERT INTO _PermissionToRole VALUES('clnf2zvlr000bpcouf4cg3x72','clnf2zvlw000gpcour6dyyuh6');
+INSERT INTO _PermissionToRole VALUES('clnf2zvls000dpcouvzwjjzrq','clnf2zvlw000gpcour6dyyuh6');
+INSERT INTO _PermissionToRole VALUES('clnf2zvlt000fpcouk29jbmxn','clnf2zvlw000gpcour6dyyuh6');
+INSERT INTO _PermissionToRole VALUES('clnf2zvli0000pcou3zzzzome','clnf2zvlx000hpcou5dfrbegs');
+INSERT INTO _PermissionToRole VALUES('clnf2zvll0002pcouka7348re','clnf2zvlx000hpcou5dfrbegs');
+INSERT INTO _PermissionToRole VALUES('clnf2zvlm0004pcou2guvolx5','clnf2zvlx000hpcou5dfrbegs');
+INSERT INTO _PermissionToRole VALUES('clnf2zvlo0006pcouyoptc5jp','clnf2zvlx000hpcou5dfrbegs');
+INSERT INTO _PermissionToRole VALUES('clnf2zvlp0008pcou9r0fhbm8','clnf2zvlx000hpcou5dfrbegs');
+INSERT INTO _PermissionToRole VALUES('clnf2zvlq000apcouxnspejs9','clnf2zvlx000hpcou5dfrbegs');
+INSERT INTO _PermissionToRole VALUES('clnf2zvlr000cpcouy1vp6oeg','clnf2zvlx000hpcou5dfrbegs');
+INSERT INTO _PermissionToRole VALUES('clnf2zvls000epcou4ts5ui8f','clnf2zvlx000hpcou5dfrbegs');
diff --git a/examples/federation/epic-stack-remote/prisma/migrations/migration_lock.toml b/examples/federation/epic-stack-remote/prisma/migrations/migration_lock.toml
new file mode 100644
index 0000000..e5e5c47
--- /dev/null
+++ b/examples/federation/epic-stack-remote/prisma/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (i.e. Git)
+provider = "sqlite"
\ No newline at end of file
diff --git a/examples/federation/epic-stack-remote/prisma/schema.prisma b/examples/federation/epic-stack-remote/prisma/schema.prisma
new file mode 100644
index 0000000..975d243
--- /dev/null
+++ b/examples/federation/epic-stack-remote/prisma/schema.prisma
@@ -0,0 +1,169 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "sqlite"
+ url = env("DATABASE_URL")
+}
+
+model User {
+ id String @id @default(cuid())
+ email String @unique
+ username String @unique
+ name String?
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ image UserImage?
+ password Password?
+ notes Note[]
+ roles Role[]
+ sessions Session[]
+ connections Connection[]
+}
+
+model Note {
+ id String @id @default(cuid())
+ title String
+ content String
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade, onUpdate: Cascade)
+ ownerId String
+
+ images NoteImage[]
+
+ // non-unique foreign key
+ @@index([ownerId])
+ // This helps our order by in the user search a LOT
+ @@index([ownerId, updatedAt])
+}
+
+model NoteImage {
+ id String @id @default(cuid())
+ altText String?
+ contentType String
+ blob Bytes
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ note Note @relation(fields: [noteId], references: [id], onDelete: Cascade, onUpdate: Cascade)
+ noteId String
+
+ // non-unique foreign key
+ @@index([noteId])
+}
+
+model UserImage {
+ id String @id @default(cuid())
+ altText String?
+ contentType String
+ blob Bytes
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
+ userId String @unique
+}
+
+model Password {
+ hash String
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
+ userId String @unique
+}
+
+model Session {
+ id String @id @default(cuid())
+ expirationDate DateTime
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
+ userId String
+
+ // non-unique foreign key
+ @@index([userId])
+}
+
+model Permission {
+ id String @id @default(cuid())
+ action String // e.g. create, read, update, delete
+ entity String // e.g. note, user, etc.
+ access String // e.g. own or any
+ description String @default("")
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ roles Role[]
+
+ @@unique([action, entity, access])
+}
+
+model Role {
+ id String @id @default(cuid())
+ name String @unique
+ description String @default("")
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ users User[]
+ permissions Permission[]
+}
+
+model Verification {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+
+ /// The type of verification, e.g. "email" or "phone"
+ type String
+
+ /// The thing we're trying to verify, e.g. a user's email or phone number
+ target String
+
+ /// The secret key used to generate the otp
+ secret String
+
+ /// The algorithm used to generate the otp
+ algorithm String
+
+ /// The number of digits in the otp
+ digits Int
+
+ /// The number of seconds the otp is valid for
+ period Int
+
+ /// The valid characters for the otp
+ charSet String
+
+ /// When it's safe to delete this verification
+ expiresAt DateTime?
+
+ @@unique([target, type])
+}
+
+model Connection {
+ id String @id @default(cuid())
+ providerName String
+ providerId String
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
+ userId String
+
+ @@unique([providerName, providerId])
+}
diff --git a/examples/federation/epic-stack-remote/prisma/seed.ts b/examples/federation/epic-stack-remote/prisma/seed.ts
new file mode 100644
index 0000000..21bca62
--- /dev/null
+++ b/examples/federation/epic-stack-remote/prisma/seed.ts
@@ -0,0 +1,230 @@
+import { faker } from '@faker-js/faker'
+import { promiseHash } from 'remix-utils/promise'
+import { prisma } from '#app/utils/db.server.ts'
+import { MOCK_CODE_GITHUB } from '#app/utils/providers/constants'
+import {
+ createPassword,
+ createUser,
+ getNoteImages,
+ getUserImages,
+ img,
+} from '#tests/db-utils.ts'
+import { insertGitHubUser } from '#tests/mocks/github.ts'
+
+async function seed() {
+ console.log('π± Seeding...')
+ console.time(`π± Database has been seeded`)
+
+ const totalUsers = 5
+ console.time(`π€ Created ${totalUsers} users...`)
+ const noteImages = await getNoteImages()
+ const userImages = await getUserImages()
+
+ for (let index = 0; index < totalUsers; index++) {
+ const userData = createUser()
+ await prisma.user
+ .create({
+ select: { id: true },
+ data: {
+ ...userData,
+ password: { create: createPassword(userData.username) },
+ image: { create: userImages[index % userImages.length] },
+ roles: { connect: { name: 'user' } },
+ notes: {
+ create: Array.from({
+ length: faker.number.int({ min: 1, max: 3 }),
+ }).map(() => ({
+ title: faker.lorem.sentence(),
+ content: faker.lorem.paragraphs(),
+ images: {
+ create: Array.from({
+ length: faker.number.int({ min: 1, max: 3 }),
+ }).map(() => {
+ const imgNumber = faker.number.int({ min: 0, max: 9 })
+ const img = noteImages[imgNumber]
+ if (!img) {
+ throw new Error(`Could not find image #${imgNumber}`)
+ }
+ return img
+ }),
+ },
+ })),
+ },
+ },
+ })
+ .catch((e) => {
+ console.error('Error creating a user:', e)
+ return null
+ })
+ }
+ console.timeEnd(`π€ Created ${totalUsers} users...`)
+
+ console.time(`π¨ Created admin user "kody"`)
+
+ const kodyImages = await promiseHash({
+ kodyUser: img({ filepath: './tests/fixtures/images/user/kody.png' }),
+ cuteKoala: img({
+ altText: 'an adorable koala cartoon illustration',
+ filepath: './tests/fixtures/images/kody-notes/cute-koala.png',
+ }),
+ koalaEating: img({
+ altText: 'a cartoon illustration of a koala in a tree eating',
+ filepath: './tests/fixtures/images/kody-notes/koala-eating.png',
+ }),
+ koalaCuddle: img({
+ altText: 'a cartoon illustration of koalas cuddling',
+ filepath: './tests/fixtures/images/kody-notes/koala-cuddle.png',
+ }),
+ mountain: img({
+ altText: 'a beautiful mountain covered in snow',
+ filepath: './tests/fixtures/images/kody-notes/mountain.png',
+ }),
+ koalaCoder: img({
+ altText: 'a koala coding at the computer',
+ filepath: './tests/fixtures/images/kody-notes/koala-coder.png',
+ }),
+ koalaMentor: img({
+ altText:
+ 'a koala in a friendly and helpful posture. The Koala is standing next to and teaching a woman who is coding on a computer and shows positive signs of learning and understanding what is being explained.',
+ filepath: './tests/fixtures/images/kody-notes/koala-mentor.png',
+ }),
+ koalaSoccer: img({
+ altText: 'a cute cartoon koala kicking a soccer ball on a soccer field ',
+ filepath: './tests/fixtures/images/kody-notes/koala-soccer.png',
+ }),
+ })
+
+ const githubUser = await insertGitHubUser(MOCK_CODE_GITHUB)
+
+ await prisma.user.create({
+ select: { id: true },
+ data: {
+ email: 'kody@kcd.dev',
+ username: 'kody',
+ name: 'Kody',
+ image: { create: kodyImages.kodyUser },
+ password: { create: createPassword('kodylovesyou') },
+ connections: {
+ create: { providerName: 'github', providerId: githubUser.profile.id },
+ },
+ roles: { connect: [{ name: 'admin' }, { name: 'user' }] },
+ notes: {
+ create: [
+ {
+ id: 'd27a197e',
+ title: 'Basic Koala Facts',
+ content:
+ 'Koalas are found in the eucalyptus forests of eastern Australia. They have grey fur with a cream-coloured chest, and strong, clawed feet, perfect for living in the branches of trees!',
+ images: { create: [kodyImages.cuteKoala, kodyImages.koalaEating] },
+ },
+ {
+ id: '414f0c09',
+ title: 'Koalas like to cuddle',
+ content:
+ 'Cuddly critters, koalas measure about 60cm to 85cm long, and weigh about 14kg.',
+ images: {
+ create: [kodyImages.koalaCuddle],
+ },
+ },
+ {
+ id: '260366b1',
+ title: 'Not bears',
+ content:
+ "Although you may have heard people call them koala 'bears', these awesome animals arenβt bears at all β they are in fact marsupials. A group of mammals, most marsupials have pouches where their newborns develop.",
+ },
+ {
+ id: 'bb79cf45',
+ title: 'Snowboarding Adventure',
+ content:
+ "Today was an epic day on the slopes! Shredded fresh powder with my friends, caught some sick air, and even attempted a backflip. Can't wait for the next snowy adventure!",
+ images: {
+ create: [kodyImages.mountain],
+ },
+ },
+ {
+ id: '9f4308be',
+ title: 'Onewheel Tricks',
+ content:
+ "Mastered a new trick on my Onewheel today called '180 Spin'. It's exhilarating to carve through the streets while pulling off these rad moves. Time to level up and learn more!",
+ },
+ {
+ id: '306021fb',
+ title: 'Coding Dilemma',
+ content:
+ "Stuck on a bug in my latest coding project. Need to figure out why my function isn't returning the expected output. Time to dig deep, debug, and conquer this challenge!",
+ images: {
+ create: [kodyImages.koalaCoder],
+ },
+ },
+ {
+ id: '16d4912a',
+ title: 'Coding Mentorship',
+ content:
+ "Had a fantastic coding mentoring session today with Sarah. Helped her understand the concept of recursion, and she made great progress. It's incredibly fulfilling to help others improve their coding skills.",
+ images: {
+ create: [kodyImages.koalaMentor],
+ },
+ },
+ {
+ id: '3199199e',
+ title: 'Koala Fun Facts',
+ content:
+ "Did you know that koalas sleep for up to 20 hours a day? It's because their diet of eucalyptus leaves doesn't provide much energy. But when I'm awake, I enjoy munching on leaves, chilling in trees, and being the cuddliest koala around!",
+ },
+ {
+ id: '2030ffd3',
+ title: 'Skiing Adventure',
+ content:
+ 'Spent the day hitting the slopes on my skis. The fresh powder made for some incredible runs and breathtaking views. Skiing down the mountain at top speed is an adrenaline rush like no other!',
+ images: {
+ create: [kodyImages.mountain],
+ },
+ },
+ {
+ id: 'f375a804',
+ title: 'Code Jam Success',
+ content:
+ 'Participated in a coding competition today and secured the first place! The adrenaline, the challenging problems, and the satisfaction of finding optimal solutionsβit was an amazing experience. Feeling proud and motivated to keep pushing my coding skills further!',
+ images: {
+ create: [kodyImages.koalaCoder],
+ },
+ },
+ {
+ id: '562c541b',
+ title: 'Koala Conservation Efforts',
+ content:
+ "Joined a local conservation group to protect koalas and their habitats. Together, we're planting more eucalyptus trees, raising awareness about their endangered status, and working towards a sustainable future for these adorable creatures. Every small step counts!",
+ },
+ // extra long note to test scrolling
+ {
+ id: 'f67ca40b',
+ title: 'Game day',
+ content:
+ "Just got back from the most amazing game. I've been playing soccer for a long time, but I've not once scored a goal. Well, today all that changed! I finally scored my first ever goal.\n\nI'm in an indoor league, and my team's not the best, but we're pretty good and I have fun, that's all that really matters. Anyway, I found myself at the other end of the field with the ball. It was just me and the goalie. I normally just kick the ball and hope it goes in, but the ball was already rolling toward the goal. The goalie was about to get the ball, so I had to charge. I managed to get possession of the ball just before the goalie got it. I brought it around the goalie and had a perfect shot. I screamed so loud in excitement. After all these years playing, I finally scored a goal!\n\nI know it's not a lot for most folks, but it meant a lot to me. We did end up winning the game by one. It makes me feel great that I had a part to play in that.\n\nIn this team, I'm the captain. I'm constantly cheering my team on. Even after getting injured, I continued to come and watch from the side-lines. I enjoy yelling (encouragingly) at my team mates and helping them be the best they can. I'm definitely not the best player by a long stretch. But I really enjoy the game. It's a great way to get exercise and have good social interactions once a week.\n\nThat said, it can be hard to keep people coming and paying dues and stuff. If people don't show up it can be really hard to find subs. I have a list of people I can text, but sometimes I can't find anyone.\n\nBut yeah, today was awesome. I felt like more than just a player that gets in the way of the opposition, but an actual asset to the team. Really great feeling.\n\nAnyway, I'm rambling at this point and really this is just so we can have a note that's pretty long to test things out. I think it's long enough now... Cheers!",
+ images: {
+ create: [kodyImages.koalaSoccer],
+ },
+ },
+ ],
+ },
+ },
+ })
+ console.timeEnd(`π¨ Created admin user "kody"`)
+
+ console.timeEnd(`π± Database has been seeded`)
+}
+
+seed()
+ .catch((e) => {
+ console.error(e)
+ process.exit(1)
+ })
+ .finally(async () => {
+ await prisma.$disconnect()
+ })
+
+// we're ok to import from the test directory in this file
+/*
+eslint
+ no-restricted-imports: "off",
+*/
diff --git a/examples/federation/epic-stack-remote/public/favicon.ico b/examples/federation/epic-stack-remote/public/favicon.ico
new file mode 100644
index 0000000..01deac7
Binary files /dev/null and b/examples/federation/epic-stack-remote/public/favicon.ico differ
diff --git a/examples/federation/epic-stack-remote/public/favicons/README.md b/examples/federation/epic-stack-remote/public/favicons/README.md
new file mode 100644
index 0000000..e28c8f4
--- /dev/null
+++ b/examples/federation/epic-stack-remote/public/favicons/README.md
@@ -0,0 +1,12 @@
+# Favicon
+
+This directory has the icons used for android devices. In some cases, we cannot
+reliably detect light/dark mode preference. Hence these icons should not have a
+transparent background. These icons are referenced in the `site.webmanifest`
+file.
+
+The icons used by modern browsers and Apple devices are in `app/assets/favicons`
+as they can be imported with a fingerprint to bust the browser cache.
+
+Note, there's also a `favicon.ico` in the root of `/public` which some older
+browsers will request automatically. This is a fallback for those browsers.
diff --git a/examples/federation/epic-stack-remote/public/favicons/android-chrome-192x192.png b/examples/federation/epic-stack-remote/public/favicons/android-chrome-192x192.png
new file mode 100644
index 0000000..f24aaa2
Binary files /dev/null and b/examples/federation/epic-stack-remote/public/favicons/android-chrome-192x192.png differ
diff --git a/examples/federation/epic-stack-remote/public/favicons/android-chrome-512x512.png b/examples/federation/epic-stack-remote/public/favicons/android-chrome-512x512.png
new file mode 100644
index 0000000..67125b8
Binary files /dev/null and b/examples/federation/epic-stack-remote/public/favicons/android-chrome-512x512.png differ
diff --git a/examples/federation/epic-stack-remote/public/img/user.png b/examples/federation/epic-stack-remote/public/img/user.png
new file mode 100644
index 0000000..683fcee
Binary files /dev/null and b/examples/federation/epic-stack-remote/public/img/user.png differ
diff --git a/examples/federation/epic-stack-remote/public/site.webmanifest b/examples/federation/epic-stack-remote/public/site.webmanifest
new file mode 100644
index 0000000..1bf9260
--- /dev/null
+++ b/examples/federation/epic-stack-remote/public/site.webmanifest
@@ -0,0 +1,20 @@
+{
+ "name": "Epic Notes",
+ "short_name": "Epic Notes",
+ "start_url": "/",
+ "icons": [
+ {
+ "src": "/favicons/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/favicons/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#A9ADC1",
+ "background_color": "#1f2028",
+ "display": "standalone"
+}
diff --git a/examples/federation/epic-stack-remote/react-router.config.ts b/examples/federation/epic-stack-remote/react-router.config.ts
new file mode 100644
index 0000000..9d4a2e9
--- /dev/null
+++ b/examples/federation/epic-stack-remote/react-router.config.ts
@@ -0,0 +1,9 @@
+import { type Config } from '@react-router/dev/config'
+export default {
+ // Defaults to true. Set to false to enable SPA for all routes.
+ ssr: true,
+
+ future: {
+ unstable_optimizeDeps: true,
+ },
+} satisfies Config
diff --git a/examples/federation/epic-stack-remote/remix.init/gitignore b/examples/federation/epic-stack-remote/remix.init/gitignore
new file mode 100644
index 0000000..86fe2f1
--- /dev/null
+++ b/examples/federation/epic-stack-remote/remix.init/gitignore
@@ -0,0 +1,29 @@
+node_modules
+.DS_store
+
+/build
+/public/build
+/server-build
+.env
+.cache
+
+/prisma/data.db
+/prisma/data.db-journal
+/tests/prisma
+
+/test-results/
+/playwright-report/
+/playwright/.cache/
+/tests/fixtures/email/
+/coverage
+
+/other/cache.db
+
+# Easy way to create temporary files/folders that won't accidentally be added to git
+*.local.*
+
+# generated files
+/app/components/ui/icons
+
+# react router v7 generated types
+.react-router
diff --git a/examples/federation/epic-stack-remote/remix.init/index.js b/examples/federation/epic-stack-remote/remix.init/index.js
new file mode 100644
index 0000000..e5e36c0
--- /dev/null
+++ b/examples/federation/epic-stack-remote/remix.init/index.js
@@ -0,0 +1,4 @@
+module.exports = async (...args) => {
+ const { default: main } = await import('./index.mjs')
+ await main(...args)
+}
diff --git a/examples/federation/epic-stack-remote/remix.init/index.mjs b/examples/federation/epic-stack-remote/remix.init/index.mjs
new file mode 100644
index 0000000..2867c16
--- /dev/null
+++ b/examples/federation/epic-stack-remote/remix.init/index.mjs
@@ -0,0 +1,315 @@
+import { execSync } from 'node:child_process'
+import crypto from 'node:crypto'
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import toml from '@iarna/toml'
+import { $ } from 'execa'
+import inquirer from 'inquirer'
+import open from 'open'
+import parseGitHubURL from 'parse-github-url'
+
+const escapeRegExp = (string) =>
+ // $& means the whole matched string
+ string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+
+const getRandomString = (length) => crypto.randomBytes(length).toString('hex')
+const getRandomString32 = () => getRandomString(32)
+
+export default async function main({ rootDirectory }) {
+ const FLY_TOML_PATH = path.join(rootDirectory, 'fly.toml')
+ const EXAMPLE_ENV_PATH = path.join(rootDirectory, '.env.example')
+ const ENV_PATH = path.join(rootDirectory, '.env')
+ const PKG_PATH = path.join(rootDirectory, 'package.json')
+
+ const appNameRegex = escapeRegExp('epic-stack-template')
+
+ const DIR_NAME = path.basename(rootDirectory)
+ const SUFFIX = getRandomString(2)
+
+ const APP_NAME = (DIR_NAME + '-' + SUFFIX)
+ // get rid of anything that's not allowed in an app name
+ .replace(/[^a-zA-Z0-9-_]/g, '-')
+ .toLowerCase()
+
+ const [flyTomlContent, env, packageJsonString] = await Promise.all([
+ fs.readFile(FLY_TOML_PATH, 'utf-8'),
+ fs.readFile(EXAMPLE_ENV_PATH, 'utf-8'),
+ fs.readFile(PKG_PATH, 'utf-8'),
+ ])
+
+ const newEnv = env.replace(
+ /^SESSION_SECRET=.*$/m,
+ `SESSION_SECRET="${getRandomString(16)}"`,
+ )
+
+ const newFlyTomlContent = flyTomlContent.replace(
+ new RegExp(appNameRegex, 'g'),
+ APP_NAME,
+ )
+
+ const packageJson = JSON.parse(packageJsonString)
+
+ packageJson.name = APP_NAME
+ delete packageJson.author
+ delete packageJson.license
+
+ const fileOperationPromises = [
+ fs.writeFile(FLY_TOML_PATH, newFlyTomlContent),
+ fs.writeFile(ENV_PATH, newEnv),
+ fs.writeFile(PKG_PATH, JSON.stringify(packageJson, null, 2)),
+ fs.copyFile(
+ path.join(rootDirectory, 'remix.init', 'gitignore'),
+ path.join(rootDirectory, '.gitignore'),
+ ),
+ fs.rm(path.join(rootDirectory, 'LICENSE.md')),
+ fs.rm(path.join(rootDirectory, 'CONTRIBUTING.md')),
+ fs.rm(path.join(rootDirectory, 'docs'), { recursive: true }),
+ fs.rm(path.join(rootDirectory, 'tests/e2e/notes.test.ts')),
+ fs.rm(path.join(rootDirectory, 'tests/e2e/search.test.ts')),
+ fs.rm(path.join(rootDirectory, '.github/workflows/version.yml')),
+ ]
+
+ await Promise.all(fileOperationPromises)
+
+ if (!process.env.SKIP_SETUP) {
+ execSync('npm run setup', { cwd: rootDirectory, stdio: 'inherit' })
+ }
+
+ if (!process.env.SKIP_FORMAT) {
+ execSync('npm run format -- --log-level warn', {
+ cwd: rootDirectory,
+ stdio: 'inherit',
+ })
+ }
+
+ if (!process.env.SKIP_DEPLOYMENT) {
+ await setupDeployment({ rootDirectory }).catch((error) => {
+ console.error(error)
+
+ console.error(
+ `Looks like something went wrong setting up deployment. Sorry about that. Check the docs for instructions on how to get deployment setup yourself (https://github.com/epicweb-dev/epic-stack/blob/main/docs/deployment.md).`,
+ )
+ })
+ }
+
+ console.log(
+ `
+Setup is complete. You're now ready to rock and roll π¨
+
+What's next?
+
+- Start development with \`npm run dev\`
+- Run tests with \`npm run test\` and \`npm run test:e2e\`
+ `.trim(),
+ )
+}
+
+async function setupDeployment({ rootDirectory }) {
+ const $I = $({ stdio: 'inherit', cwd: rootDirectory })
+
+ const { shouldSetupDeployment } = await inquirer.prompt([
+ {
+ name: 'shouldSetupDeployment',
+ type: 'confirm',
+ default: true,
+ message: 'Would you like to set up deployment right now?',
+ },
+ ])
+
+ if (!shouldSetupDeployment) {
+ console.log(
+ `Ok, check the docs (https://github.com/epicweb-dev/epic-stack/blob/main/docs/deployment.md) when you're ready to set that up.`,
+ )
+ return
+ }
+
+ const hasFly = await $`fly version`.then(
+ () => true,
+ () => false,
+ )
+ if (!hasFly) {
+ console.log(
+ `You need to install Fly first. Follow the instructions here: https://fly.io/docs/hands-on/install-flyctl/`,
+ )
+ return
+ }
+ const loggedInUser = await ensureLoggedIn()
+ if (!loggedInUser) {
+ console.log(
+ `Ok, check the docs when you're ready to get this deployed: https://github.com/epicweb-dev/epic-stack/blob/main/docs/deployment.md`,
+ )
+ }
+
+ console.log('π Determining the best region for you...')
+ const primaryRegion = await getPreferredRegion()
+
+ const flyConfig = toml.parse(
+ await fs.readFile(path.join(rootDirectory, 'fly.toml')),
+ )
+ flyConfig.primary_region = primaryRegion
+ await fs.writeFile(
+ path.join(rootDirectory, 'fly.toml'),
+ toml.stringify(flyConfig),
+ )
+
+ const { app: APP_NAME } = flyConfig
+
+ console.log(`π₯ͺ Creating app ${APP_NAME} and ${APP_NAME}-staging...`)
+ await $I`fly apps create ${APP_NAME}-staging`
+ await $I`fly apps create ${APP_NAME}`
+
+ console.log(`π€« Setting secrets in apps`)
+ await $I`fly secrets set SESSION_SECRET=${getRandomString32()} INTERNAL_COMMAND_TOKEN=${getRandomString32()} HONEYPOT_SECRET=${getRandomString32()} ALLOW_INDEXING=false --app ${APP_NAME}-staging`
+ await $I`fly secrets set SESSION_SECRET=${getRandomString32()} INTERNAL_COMMAND_TOKEN=${getRandomString32()} HONEYPOT_SECRET=${getRandomString32()} --app ${APP_NAME}`
+
+ console.log(
+ `π Creating volumes. Answer "yes" when it warns you about downtime. You can add more volumes later (when you actually start getting paying customers οΏ½).`,
+ )
+ await $I`fly volumes create data --region ${primaryRegion} --size 1 --app ${APP_NAME}-staging`
+ await $I`fly volumes create data --region ${primaryRegion} --size 1 --app ${APP_NAME}`
+
+ // attach consul
+ console.log(`π Attaching consul`)
+ await $I`fly consul attach --app ${APP_NAME}-staging`
+ await $I`fly consul attach --app ${APP_NAME}`
+
+ const { shouldDeploy } = await inquirer.prompt([
+ {
+ name: 'shouldDeploy',
+ type: 'confirm',
+ default: true,
+ message:
+ 'Would you like to deploy right now? (This will take a while, and you can always wait until you push to GitHub instead).',
+ },
+ ])
+ if (shouldDeploy) {
+ console.log(`π Deploying apps...`)
+ console.log(` Starting with staging`)
+ await $I`fly deploy --app ${APP_NAME}-staging`
+ await open(`https://${APP_NAME}-staging.fly.dev/`)
+
+ console.log(` Staging deployed... Deploying production...`)
+ await $I`fly deploy --app ${APP_NAME}`
+ await open(`https://${APP_NAME}.fly.dev/`)
+ console.log(` Production deployed...`)
+ }
+
+ const { shouldSetupGitHub } = await inquirer.prompt([
+ {
+ name: 'shouldSetupGitHub',
+ type: 'confirm',
+ default: true,
+ message: 'Would you like to setup GitHub Action deployment right now?',
+ },
+ ])
+ if (shouldSetupGitHub) {
+ console.log(`β Initializing git repo...`)
+ // it's possible there's already a git repo initialized so we'll just ignore
+ // any errors and hope things work out.
+ await $I`git init`.catch(() => {})
+
+ console.log(
+ `Opening repo.new. Please create a new repo and paste the URL below.`,
+ )
+ await open(`https://repo.new`)
+
+ const { repoURL } = await inquirer.prompt([
+ {
+ name: 'repoURL',
+ type: 'input',
+ message: 'What is the URL of your repo?',
+ },
+ ])
+
+ const githubParts = parseGitHubURL(repoURL)
+
+ if (!githubParts) {
+ throw new Error(`Invalid GitHub URL: ${repoURL}`)
+ }
+
+ console.log(
+ `Opening Fly Tokens Dashboard and GitHub Action Secrets pages. Please create a new token on Fly and set it as the value for a new secret called FLY_API_TOKEN on GitHub.`,
+ )
+ await open(`https://web.fly.io/user/personal_access_tokens/new`)
+ await open(`${repoURL}/settings/secrets/actions/new`)
+
+ console.log(
+ `Once you're finished with setting the token, you should be good to add the remote, commit, and push!`,
+ )
+ }
+ console.log('All done π Happy building')
+}
+
+async function ensureLoggedIn() {
+ const loggedInUser = await $`fly auth whoami`.then(
+ ({ stdout }) => stdout,
+ () => null,
+ )
+ if (loggedInUser) {
+ const answers = await inquirer.prompt([
+ {
+ name: 'proceed',
+ type: 'list',
+ default: 'Yes',
+ message: `You're logged in as ${loggedInUser}. Proceed?`,
+ choices: ['Yes', 'Login as another user', 'Exit'],
+ },
+ ])
+ switch (answers.proceed) {
+ case 'Yes': {
+ return loggedInUser
+ }
+ case 'Login as another user': {
+ await $`fly auth logout`
+ return ensureLoggedIn()
+ }
+ default: {
+ return null
+ }
+ }
+ } else {
+ console.log(`You need to login to Fly first. Running \`fly auth login\`...`)
+ await $({ stdio: 'inherit' })`fly auth login`
+ return ensureLoggedIn()
+ }
+}
+
+async function getPreferredRegion() {
+ const {
+ platform: { requestRegion: defaultRegion },
+ } = await makeFlyRequest({ query: 'query {platform {requestRegion}}' })
+
+ const availableRegions = await makeFlyRequest({
+ query: `{platform {regions {name code}}}`,
+ })
+ const { preferredRegion } = await inquirer.prompt([
+ {
+ name: 'preferredRegion',
+ type: 'list',
+ default: defaultRegion,
+ message: `Which region would you like to deploy to? The closest to you is ${defaultRegion}.`,
+ choices: availableRegions.platform.regions.map((region) => ({
+ name: `${region.name} (${region.code})`,
+ value: region.code,
+ })),
+ },
+ ])
+ return preferredRegion
+}
+
+let flyToken = null
+async function makeFlyRequest({ query, variables }) {
+ if (!flyToken) {
+ flyToken = (await $`fly auth token`).stdout.trim()
+ }
+
+ const json = await fetch('https://api.fly.io/graphql', {
+ method: 'POST',
+ body: JSON.stringify({ query, variables }),
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${flyToken}`,
+ },
+ }).then((response) => response.json())
+ return json.data
+}
diff --git a/examples/federation/epic-stack-remote/remix.init/package-lock.json b/examples/federation/epic-stack-remote/remix.init/package-lock.json
new file mode 100644
index 0000000..d3755cc
--- /dev/null
+++ b/examples/federation/epic-stack-remote/remix.init/package-lock.json
@@ -0,0 +1,1731 @@
+{
+ "name": "remix.init",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "remix.init",
+ "dependencies": {
+ "@iarna/toml": "^2.2.5",
+ "execa": "^7.1.1",
+ "inquirer": "^9.2.6",
+ "open": "^9.1.0",
+ "parse-github-url": "^1.0.2"
+ }
+ },
+ "node_modules/@iarna/toml": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz",
+ "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/big-integer": {
+ "version": "1.6.51",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
+ "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bplist-parser": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz",
+ "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==",
+ "dependencies": {
+ "big-integer": "^1.6.44"
+ },
+ "engines": {
+ "node": ">= 5.10.0"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/bundle-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz",
+ "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==",
+ "dependencies": {
+ "run-applescript": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz",
+ "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
+ },
+ "node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-spinners": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz",
+ "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-width": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.0.0.tgz",
+ "integrity": "sha512-ZksGS2xpa/bYkNzN3BAw1wEjsLV/ZKOf/CCrJ/QOBsxx6fOARIkwTutxp1XIOIohi6HKmOFjMoK/XaqDVUpEEw==",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/default-browser": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz",
+ "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==",
+ "dependencies": {
+ "bundle-name": "^3.0.0",
+ "default-browser-id": "^3.0.0",
+ "execa": "^7.1.1",
+ "titleize": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser-id": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz",
+ "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==",
+ "dependencies": {
+ "bplist-parser": "^0.2.0",
+ "untildify": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/defaults": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
+ "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+ "dependencies": {
+ "clone": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/execa": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz",
+ "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.1",
+ "human-signals": "^4.3.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^3.0.7",
+ "strip-final-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || ^16.14.0 || >=18.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/execa/node_modules/mimic-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/execa/node_modules/onetime": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "dependencies": {
+ "mimic-fn": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/external-editor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+ "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "dependencies": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/figures": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz",
+ "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==",
+ "dependencies": {
+ "escape-string-regexp": "^5.0.0",
+ "is-unicode-supported": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
+ "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==",
+ "engines": {
+ "node": ">=14.18.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/inquirer": {
+ "version": "9.2.6",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.6.tgz",
+ "integrity": "sha512-y71l237eJJKS4rl7sQcEUiMhrR0pB/ZnRMMTxLpjJhWL4hdWCT03a6jJnC1w6qIPSRZWEozuieGt3v7XaEJYFw==",
+ "dependencies": {
+ "ansi-escapes": "^4.3.2",
+ "chalk": "^5.2.0",
+ "cli-cursor": "^3.1.0",
+ "cli-width": "^4.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^5.0.0",
+ "lodash": "^4.17.21",
+ "mute-stream": "1.0.0",
+ "ora": "^5.4.1",
+ "run-async": "^3.0.0",
+ "rxjs": "^7.8.1",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "through": "^2.3.6",
+ "wrap-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=14.18.0"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "dependencies": {
+ "is-docker": "^3.0.0"
+ },
+ "bin": {
+ "is-inside-container": "cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-interactive": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
+ "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
+ "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-wsl/node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-symbols/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/log-symbols/node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mute-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
+ "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
+ "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==",
+ "dependencies": {
+ "path-key": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/open": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz",
+ "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==",
+ "dependencies": {
+ "default-browser": "^4.0.0",
+ "define-lazy-prop": "^3.0.0",
+ "is-inside-container": "^1.0.0",
+ "is-wsl": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
+ "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
+ "dependencies": {
+ "bl": "^4.1.0",
+ "chalk": "^4.1.0",
+ "cli-cursor": "^3.1.0",
+ "cli-spinners": "^2.5.0",
+ "is-interactive": "^1.0.0",
+ "is-unicode-supported": "^0.1.0",
+ "log-symbols": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "wcwidth": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/ora/node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/parse-github-url": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.2.tgz",
+ "integrity": "sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==",
+ "bin": {
+ "parse-github-url": "cli.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/run-applescript": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz",
+ "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==",
+ "dependencies": {
+ "execa": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/run-applescript/node_modules/execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/run-applescript/node_modules/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/run-applescript/node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/run-applescript/node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/run-applescript/node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/run-async": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
+ "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
+ },
+ "node_modules/titleize": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
+ "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "dependencies": {
+ "os-tmpdir": "~1.0.2"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz",
+ "integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA=="
+ },
+ "node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/untildify": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
+ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+ },
+ "node_modules/wcwidth": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+ "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
+ "dependencies": {
+ "defaults": "^1.0.3"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ }
+ },
+ "dependencies": {
+ "@iarna/toml": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz",
+ "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="
+ },
+ "ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "requires": {
+ "type-fest": "^0.21.3"
+ }
+ },
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
+ },
+ "big-integer": {
+ "version": "1.6.51",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
+ "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg=="
+ },
+ "bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "requires": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "bplist-parser": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz",
+ "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==",
+ "requires": {
+ "big-integer": "^1.6.44"
+ }
+ },
+ "buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "requires": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "bundle-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz",
+ "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==",
+ "requires": {
+ "run-applescript": "^5.0.0"
+ }
+ },
+ "chalk": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz",
+ "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA=="
+ },
+ "chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
+ },
+ "cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "requires": {
+ "restore-cursor": "^3.1.0"
+ }
+ },
+ "cli-spinners": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz",
+ "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g=="
+ },
+ "cli-width": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.0.0.tgz",
+ "integrity": "sha512-ZksGS2xpa/bYkNzN3BAw1wEjsLV/ZKOf/CCrJ/QOBsxx6fOARIkwTutxp1XIOIohi6HKmOFjMoK/XaqDVUpEEw=="
+ },
+ "clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "requires": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ }
+ },
+ "default-browser": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz",
+ "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==",
+ "requires": {
+ "bundle-name": "^3.0.0",
+ "default-browser-id": "^3.0.0",
+ "execa": "^7.1.1",
+ "titleize": "^3.0.0"
+ }
+ },
+ "default-browser-id": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz",
+ "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==",
+ "requires": {
+ "bplist-parser": "^0.2.0",
+ "untildify": "^4.0.0"
+ }
+ },
+ "defaults": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
+ "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+ "requires": {
+ "clone": "^1.0.2"
+ }
+ },
+ "define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="
+ },
+ "execa": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz",
+ "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==",
+ "requires": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.1",
+ "human-signals": "^4.3.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^3.0.7",
+ "strip-final-newline": "^3.0.0"
+ },
+ "dependencies": {
+ "mimic-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="
+ },
+ "onetime": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "requires": {
+ "mimic-fn": "^4.0.0"
+ }
+ }
+ }
+ },
+ "external-editor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+ "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "requires": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ }
+ },
+ "figures": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz",
+ "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==",
+ "requires": {
+ "escape-string-regexp": "^5.0.0",
+ "is-unicode-supported": "^1.2.0"
+ }
+ },
+ "get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+ },
+ "human-signals": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
+ "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ=="
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "inquirer": {
+ "version": "9.2.6",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.6.tgz",
+ "integrity": "sha512-y71l237eJJKS4rl7sQcEUiMhrR0pB/ZnRMMTxLpjJhWL4hdWCT03a6jJnC1w6qIPSRZWEozuieGt3v7XaEJYFw==",
+ "requires": {
+ "ansi-escapes": "^4.3.2",
+ "chalk": "^5.2.0",
+ "cli-cursor": "^3.1.0",
+ "cli-width": "^4.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^5.0.0",
+ "lodash": "^4.17.21",
+ "mute-stream": "1.0.0",
+ "ora": "^5.4.1",
+ "run-async": "^3.0.0",
+ "rxjs": "^7.8.1",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "through": "^2.3.6",
+ "wrap-ansi": "^6.0.1"
+ }
+ },
+ "is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
+ },
+ "is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "requires": {
+ "is-docker": "^3.0.0"
+ }
+ },
+ "is-interactive": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
+ "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="
+ },
+ "is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="
+ },
+ "is-unicode-supported": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
+ "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="
+ },
+ "is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "requires": {
+ "is-docker": "^2.0.0"
+ },
+ "dependencies": {
+ "is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="
+ }
+ }
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+ },
+ "lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ },
+ "log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "requires": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "dependencies": {
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="
+ }
+ }
+ },
+ "merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
+ },
+ "mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
+ },
+ "mute-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
+ "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="
+ },
+ "npm-run-path": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
+ "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==",
+ "requires": {
+ "path-key": "^4.0.0"
+ },
+ "dependencies": {
+ "path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="
+ }
+ }
+ },
+ "onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "requires": {
+ "mimic-fn": "^2.1.0"
+ }
+ },
+ "open": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz",
+ "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==",
+ "requires": {
+ "default-browser": "^4.0.0",
+ "define-lazy-prop": "^3.0.0",
+ "is-inside-container": "^1.0.0",
+ "is-wsl": "^2.2.0"
+ }
+ },
+ "ora": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
+ "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
+ "requires": {
+ "bl": "^4.1.0",
+ "chalk": "^4.1.0",
+ "cli-cursor": "^3.1.0",
+ "cli-spinners": "^2.5.0",
+ "is-interactive": "^1.0.0",
+ "is-unicode-supported": "^0.1.0",
+ "log-symbols": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "wcwidth": "^1.0.1"
+ },
+ "dependencies": {
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="
+ }
+ }
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="
+ },
+ "parse-github-url": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.2.tgz",
+ "integrity": "sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw=="
+ },
+ "path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
+ },
+ "readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ },
+ "restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "requires": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "run-applescript": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz",
+ "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==",
+ "requires": {
+ "execa": "^5.0.0"
+ },
+ "dependencies": {
+ "execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "requires": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ }
+ },
+ "human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="
+ },
+ "is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="
+ },
+ "npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "requires": {
+ "path-key": "^3.0.0"
+ }
+ },
+ "strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="
+ }
+ }
+ },
+ "run-async": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
+ "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q=="
+ },
+ "rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+ "requires": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "requires": {
+ "shebang-regex": "^3.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
+ },
+ "signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
+ },
+ "string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "requires": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
+ },
+ "titleize": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
+ "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ=="
+ },
+ "tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "requires": {
+ "os-tmpdir": "~1.0.2"
+ }
+ },
+ "tslib": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz",
+ "integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA=="
+ },
+ "type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="
+ },
+ "untildify": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
+ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw=="
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+ },
+ "wcwidth": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+ "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
+ "requires": {
+ "defaults": "^1.0.3"
+ }
+ },
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ }
+ }
+ }
+}
diff --git a/examples/federation/epic-stack-remote/remix.init/package.json b/examples/federation/epic-stack-remote/remix.init/package.json
new file mode 100644
index 0000000..3a85b81
--- /dev/null
+++ b/examples/federation/epic-stack-remote/remix.init/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "remix.init",
+ "private": true,
+ "main": "index.js",
+ "dependencies": {
+ "@iarna/toml": "^2.2.5",
+ "execa": "^7.1.1",
+ "inquirer": "^9.2.6",
+ "open": "^9.1.0",
+ "parse-github-url": "^1.0.2"
+ }
+}
diff --git a/examples/federation/epic-stack-remote/rsbuild.config.ts b/examples/federation/epic-stack-remote/rsbuild.config.ts
new file mode 100644
index 0000000..8552e7f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/rsbuild.config.ts
@@ -0,0 +1,150 @@
+import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack'
+import { defineConfig } from '@rsbuild/core'
+import { pluginReact } from '@rsbuild/plugin-react'
+import { pluginReactRouter } from '@rsbuild/plugin-react-router'
+import type { Compiler } from '@rspack/core'
+
+import 'react-router'
+
+
+// Common shared dependencies for Module Federation
+const sharedDependencies = {
+ 'react-router': {
+ singleton: true,
+ },
+ 'react-router/': {
+ singleton: true,
+ },
+ react: {
+ singleton: true,
+ },
+ 'react/': {
+ singleton: true,
+ },
+ 'react-dom': {
+ singleton: true,
+ },
+ 'react-dom/': {
+ singleton: true,
+ },
+}
+
+// Common exposed components
+const exposedComponents = {
+ './components/search-bar': './app/components/search-bar',
+ './components/user-dropdown': './app/components/user-dropdown',
+ './components/spacer': './app/components/spacer',
+ './components/toaster': './app/components/toaster',
+ './components/error-boundary': './app/components/error-boundary',
+ './components/floating-toolbar': './app/components/floating-toolbar',
+ './components/forms': './app/components/forms',
+ './components/progress-bar': './app/components/progress-bar',
+ './components/ui/tooltip': './app/components/ui/tooltip',
+ './components/ui/status-button': './app/components/ui/status-button',
+ './components/ui/textarea': './app/components/ui/textarea',
+ './components/ui/sonner': './app/components/ui/sonner',
+ './components/ui/label': './app/components/ui/label',
+ './components/ui/input': './app/components/ui/input',
+ './components/ui/input-otp': './app/components/ui/input-otp',
+ './components/ui/dropdown-menu': './app/components/ui/dropdown-menu',
+ './components/ui/icon': './app/components/ui/icon',
+ './components/ui/button': './app/components/ui/button',
+ './components/ui/checkbox': './app/components/ui/checkbox',
+ "./utils/connections": "./app/utils/connections",
+}
+
+// Common Module Federation configuration
+const commonFederationConfig = {
+ name: 'remote',
+ shareStrategy: "loaded-first" as const,
+ runtime: undefined,
+ exposes: exposedComponents,
+ shared: sharedDependencies
+}
+
+// Web-specific federation config
+const webFederationConfig = {
+ ...commonFederationConfig,
+ library: {
+ type: 'module'
+ },
+}
+
+// Node-specific federation config
+const nodeFederationConfig = {
+ ...commonFederationConfig,
+ library: {
+ type: 'commonjs-module'
+ },
+ runtimePlugins: [
+ '@module-federation/node/runtimePlugin'
+ ],
+}
+
+export default defineConfig({
+ dev: {
+ client: {
+ overlay: false,
+ },
+ },
+ tools: {
+ rspack: {
+ devtool: false,
+ }
+ },
+ environments: {
+ web: {
+ source: {
+ define: {
+ WEB: 'true'
+ }
+ },
+ tools: {
+ rspack: {
+ plugins: [
+ new ModuleFederationPlugin(webFederationConfig)
+ ]
+ }
+ },
+ plugins: []
+ },
+ node: {
+ output: {
+ assetPrefix: 'http://localhost:3001/',
+ },
+ tools: {
+ rspack: {
+ plugins: [
+ new ModuleFederationPlugin(nodeFederationConfig)
+ ]
+ }
+ },
+ plugins: []
+ }
+ },
+ server: {
+ port: Number(process.env.PORT || 3000),
+ },
+ output: {
+ assetPrefix: 'http://localhost:3001/',
+ externals: ['better-sqlite3', 'express','ws'],
+ },
+ plugins: [
+ pluginReactRouter({ customServer: true, serverOutput: 'commonjs', federation: true }),
+ pluginReact({
+ fastRefresh: false,
+ swcReactOptions: {
+ refresh: false,
+ development: false
+ },
+ splitChunks: {
+ router: false,
+ react: false
+ },
+ reactRefreshOptions: {
+ overlay: false,
+ exclude: /root/,
+ },
+ }),
+ ],
+})
diff --git a/examples/federation/epic-stack-remote/server/dev-build.js b/examples/federation/epic-stack-remote/server/dev-build.js
new file mode 100644
index 0000000..a01dcea
--- /dev/null
+++ b/examples/federation/epic-stack-remote/server/dev-build.js
@@ -0,0 +1,23 @@
+import { createRsbuild, loadConfig } from '@rsbuild/core'
+import 'dotenv/config'
+
+async function startServer() {
+ const config = await loadConfig()
+ const rsbuild = await createRsbuild({
+ rsbuildConfig: config.content,
+ })
+ const devServer = await rsbuild.createDevServer()
+
+ // Load the bundle first to get createApp
+ if (!devServer.environments?.node) {
+ throw new Error('Node environment not found in dev server')
+ }
+
+ const bundle = await devServer.environments.node.loadBundle('app')
+ const { createApp } = bundle
+ const app = await createApp(devServer)
+
+ devServer.connectWebSocket({ server: app })
+}
+
+void startServer().catch(console.error)
diff --git a/examples/federation/epic-stack-remote/server/dev-server.js b/examples/federation/epic-stack-remote/server/dev-server.js
new file mode 100644
index 0000000..2d824d2
--- /dev/null
+++ b/examples/federation/epic-stack-remote/server/dev-server.js
@@ -0,0 +1,7 @@
+import { execa } from 'execa'
+
+if (process.env.NODE_ENV === 'production') {
+ await import('../server-build/index.js')
+} else {
+ await import('../index.js')
+}
diff --git a/examples/federation/epic-stack-remote/server/index.ts b/examples/federation/epic-stack-remote/server/index.ts
new file mode 100644
index 0000000..0e6451b
--- /dev/null
+++ b/examples/federation/epic-stack-remote/server/index.ts
@@ -0,0 +1,319 @@
+import crypto from 'node:crypto'
+import { createRequestHandler } from '@react-router/express'
+import * as Sentry from '@sentry/node'
+import { ip as ipAddress } from 'address'
+import chalk from 'chalk'
+import closeWithGrace from 'close-with-grace'
+import compression from 'compression'
+import express, { type RequestHandler } from 'express'
+import rateLimit from 'express-rate-limit'
+import getPort, { portNumbers } from 'get-port'
+import helmet from 'helmet'
+import morgan from 'morgan'
+import { type ServerBuild } from 'react-router'
+
+import 'dotenv/config'
+import * as fs from 'node:fs'
+import sourceMapSupport from 'source-map-support'
+
+sourceMapSupport.install({
+ retrieveSourceMap: function (source) {
+ // get source file without the `file://` prefix or `?t=...` suffix
+ const match = source.match(/^file:\/\/(.*)\?t=[.\d]+$/)
+ if (match) {
+ return {
+ url: source,
+ map: fs.readFileSync(`${match[1]}.map`, 'utf8'),
+ }
+ }
+ return null
+ },
+})
+
+
+const MODE = process.env.NODE_ENV ?? 'development'
+const IS_PROD = MODE === 'production'
+const IS_DEV = MODE === 'development'
+const ALLOW_INDEXING = process.env.ALLOW_INDEXING !== 'false'
+const SENTRY_ENABLED = IS_PROD && process.env.SENTRY_DSN
+
+if (SENTRY_ENABLED) {
+ void import('./utils/monitoring.ts').then(({ init }) => init())
+}
+
+export async function createApp(devServer?: any) {
+ const app = express()
+
+ const getHost = (req: { get: (key: string) => string | undefined }) =>
+ req.get('X-Forwarded-Host') ?? req.get('host') ?? ''
+
+ // fly is our proxy
+ app.set('trust proxy', true)
+
+ // ensure HTTPS only (X-Forwarded-Proto comes from Fly)
+ app.use((req, res, next) => {
+ if (req.method !== 'GET') return next()
+ const proto = req.get('X-Forwarded-Proto')
+ const host = getHost(req)
+ if (proto === 'http') {
+ res.set('X-Forwarded-Proto', 'https')
+ res.redirect(`https://${host}${req.originalUrl}`)
+ return
+ }
+ next()
+ })
+
+ // no ending slashes for SEO reasons
+ // https://github.com/epicweb-dev/epic-stack/discussions/108
+ app.get('*', (req, res, next) => {
+ if (req.path.endsWith('/') && req.path.length > 1) {
+ const query = req.url.slice(req.path.length)
+ const safepath = req.path.slice(0, -1).replace(/\/+/g, '/')
+ res.redirect(302, safepath + query)
+ } else {
+ next()
+ }
+ })
+
+ app.use(compression())
+
+ // http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
+ app.disable('x-powered-by')
+
+ if (IS_DEV) {
+ // use rsbuild dev server
+ if (devServer) {
+ app.use(devServer.middlewares)
+ }
+ } else {
+ // Remix fingerprints its assets so we can cache forever.
+ app.use(
+ '/assets',
+ express.static('build/client/assets', { immutable: true, maxAge: '1y' }),
+ )
+
+ // Everything else (like favicon.ico) is cached for an hour. You may want to be
+ // more aggressive with this caching.
+ app.use(express.static('build/client', { maxAge: '1h' }))
+ app.use('/server', express.static('build/server', { maxAge: '1h' }))
+ }
+
+ app.get(['/img/*', '/favicons/*'], ((
+ _req: express.Request,
+ res: express.Response,
+ ) => {
+ // if we made it past the express.static for these, then we're missing something.
+ // So we'll just send a 404 and won't bother calling other middleware.
+ res.status(404).send('Not found')
+ }) as RequestHandler)
+
+ morgan.token('url', (req) => {
+ try {
+ return decodeURIComponent(req.url ?? '')
+ } catch {
+ return req.url ?? ''
+ }
+ })
+ app.use(
+ morgan('tiny', {
+ skip: (req, res) =>
+ res.statusCode === 200 &&
+ (req.url?.startsWith('/resources/note-images') ||
+ req.url?.startsWith('/resources/user-images') ||
+ req.url?.startsWith('/resources/healthcheck')),
+ }),
+ )
+
+ app.use((_, res, next) => {
+ res.locals.cspNonce = crypto.randomBytes(16).toString('hex')
+ next()
+ })
+
+ app.use(
+ helmet({
+ xPoweredBy: false,
+ referrerPolicy: { policy: 'same-origin' },
+ crossOriginEmbedderPolicy: false,
+ contentSecurityPolicy: {
+ // NOTE: Remove reportOnly when you're ready to enforce this CSP
+ reportOnly: true,
+ directives: {
+ 'connect-src': [
+ MODE === 'development' ? 'ws:' : null,
+ process.env.SENTRY_DSN ? '*.sentry.io' : null,
+ "'self'",
+ ].filter(Boolean),
+ 'font-src': ["'self'"],
+ 'frame-src': ["'self'"],
+ 'img-src': ["'self'", 'data:'],
+ 'script-src': [
+ "'strict-dynamic'",
+ "'self'",
+ // @ts-expect-error
+ (_, res) => `'nonce-${res.locals.cspNonce}'`,
+ ],
+ 'script-src-attr': [
+ // @ts-expect-error
+ (_, res) => `'nonce-${res.locals.cspNonce}'`,
+ ],
+ 'upgrade-insecure-requests': null,
+ },
+ },
+ }),
+ )
+
+ // When running tests or running in development, we want to effectively disable
+ // rate limiting because playwright tests are very fast and we don't want to
+ // have to wait for the rate limit to reset between tests.
+ const maxMultiple =
+ !IS_PROD || process.env.PLAYWRIGHT_TEST_BASE_URL ? 10_000 : 1
+ const rateLimitDefault = {
+ windowMs: 60 * 1000,
+ limit: 1000 * maxMultiple,
+ standardHeaders: true,
+ legacyHeaders: false,
+ validate: { trustProxy: false },
+ // Malicious users can spoof their IP address which means we should not default
+ // to trusting req.ip when hosted on Fly.io. However, users cannot spoof Fly-Client-Ip.
+ // When sitting behind a CDN such as cloudflare, replace fly-client-ip with the CDN
+ // specific header such as cf-connecting-ip
+ keyGenerator: (req: express.Request) => {
+ return req.get('fly-client-ip') ?? `${req.ip}`
+ },
+ }
+
+ const strongestRateLimit = rateLimit({
+ ...rateLimitDefault,
+ windowMs: 60 * 1000,
+ limit: 10 * maxMultiple,
+ })
+
+ const strongRateLimit = rateLimit({
+ ...rateLimitDefault,
+ windowMs: 60 * 1000,
+ limit: 100 * maxMultiple,
+ })
+
+ const generalRateLimit = rateLimit(rateLimitDefault)
+ app.use((req, res, next) => {
+ const strongPaths = [
+ '/login',
+ '/signup',
+ '/verify',
+ '/admin',
+ '/onboarding',
+ '/reset-password',
+ '/settings/profile',
+ '/resources/login',
+ '/resources/verify',
+ ]
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
+ if (strongPaths.some((p) => req.path.includes(p))) {
+ return strongestRateLimit(req, res, next)
+ }
+ return strongRateLimit(req, res, next)
+ }
+
+ // the verify route is a special case because it's a GET route that
+ // can have a token in the query string
+ if (req.path.includes('/verify')) {
+ return strongestRateLimit(req, res, next)
+ }
+
+ return generalRateLimit(req, res, next)
+ })
+
+ async function getBuild() {
+ try {
+ //@ts-ignore
+ const build = import('virtual/react-router/server-build')
+
+ return { build: build as unknown as ServerBuild, error: null }
+ } catch (error) {
+ // Catch error and return null to make express happy and avoid an unrecoverable crash
+ console.error('Error creating build:', error)
+ return { error: error, build: null as unknown as ServerBuild }
+ }
+ }
+
+ if (!ALLOW_INDEXING) {
+ app.use((_, res, next) => {
+ res.set('X-Robots-Tag', 'noindex, nofollow')
+ next()
+ })
+ }
+
+ app.all(
+ '*',
+ createRequestHandler({
+ getLoadContext: (_: any, res: any) => ({
+ cspNonce: res.locals.cspNonce,
+ serverBuild: getBuild(),
+ VALUE_FROM_EXPRESS: 'Hello from Epic Stack',
+ }),
+ mode: MODE,
+ build: async () => {
+ const { error, build } = await getBuild()
+ // gracefully "catch" the error
+ if (error) {
+ throw error
+ }
+ return build
+ },
+ }),
+ )
+
+ const desiredPort = Number(process.env.PORT || 3000)
+ const portToUse = await getPort({
+ port: portNumbers(desiredPort, desiredPort + 100),
+ })
+ const portAvailable = desiredPort === portToUse
+ if (!portAvailable && !IS_DEV) {
+ console.log(`β οΈ Port ${desiredPort} is not available.`)
+ process.exit(1)
+ }
+
+ const server = app.listen(portToUse, () => {
+ if (!portAvailable) {
+ console.warn(
+ chalk.yellow(
+ `β οΈ Port ${desiredPort} is not available, using ${portToUse} instead.`,
+ ),
+ )
+ }
+ console.log(`π We have liftoff!`)
+ const localUrl = `http://localhost:${portToUse}`
+ let lanUrl: string | null = null
+ const localIp = ipAddress() ?? 'Unknown'
+ // Check if the address is a private ip
+ // https://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
+ // https://github.com/facebook/create-react-app/blob/d960b9e38c062584ff6cfb1a70e1512509a966e7/packages/react-dev-utils/WebpackDevServerUtils.js#LL48C9-L54C10
+ if (/^10[.]|^172[.](1[6-9]|2[0-9]|3[0-1])[.]|^192[.]168[.]/.test(localIp)) {
+ lanUrl = `http://${localIp}:${portToUse}`
+ }
+
+ console.log(
+ `
+${chalk.bold('Local:')} ${chalk.cyan(localUrl)}
+${lanUrl ? `${chalk.bold('On Your Network:')} ${chalk.cyan(lanUrl)}` : ''}
+${chalk.bold('Press Ctrl+C to stop')}
+ `.trim(),
+ )
+ })
+
+ closeWithGrace(async ({ err }) => {
+ await new Promise((resolve, reject) => {
+ server.close((e) => (e ? reject(e) : resolve('ok')))
+ })
+ if (err) {
+ console.error(chalk.red(err))
+ console.error(chalk.red(err.stack))
+ if (SENTRY_ENABLED) {
+ Sentry.captureException(err)
+ await Sentry.flush(500)
+ }
+ }
+ })
+
+ return app
+}
diff --git a/examples/federation/epic-stack-remote/server/utils/monitoring.ts b/examples/federation/epic-stack-remote/server/utils/monitoring.ts
new file mode 100644
index 0000000..d36a8c8
--- /dev/null
+++ b/examples/federation/epic-stack-remote/server/utils/monitoring.ts
@@ -0,0 +1,47 @@
+import {PrismaInstrumentation} from '@prisma/instrumentation'
+import * as Sentry from '@sentry/node'
+import { nodeProfilingIntegration } from '@sentry/profiling-node'
+
+// prisma's exports are wrong...
+// https://github.com/prisma/prisma/issues/23410
+
+export function init() {
+ Sentry.init({
+ dsn: process.env.SENTRY_DSN,
+ environment: process.env.NODE_ENV,
+ tracesSampleRate: process.env.NODE_ENV === 'production' ? 1 : 0,
+ denyUrls: [
+ /\/resources\/healthcheck/,
+ // TODO: be smarter about the public assets...
+ /\/build\//,
+ /\/favicons\//,
+ /\/img\//,
+ /\/fonts\//,
+ /\/favicon.ico/,
+ /\/site\.webmanifest/,
+ ],
+ integrations: [
+ Sentry.prismaIntegration({
+ prismaInstrumentation: new PrismaInstrumentation(),
+ }),
+ Sentry.httpIntegration(),
+ nodeProfilingIntegration(),
+ ],
+ tracesSampler(samplingContext) {
+ // ignore healthcheck transactions by other services (consul, etc.)
+ if (samplingContext.request?.url?.includes('/resources/healthcheck')) {
+ return 0
+ }
+ return 1
+ },
+ beforeSendTransaction(event) {
+ // ignore all healthcheck related transactions
+ // note that name of header here is case-sensitive
+ if (event.request?.headers?.['x-healthcheck'] === 'true') {
+ return null
+ }
+
+ return event
+ },
+ })
+}
diff --git a/examples/federation/epic-stack-remote/tailwind.config.ts b/examples/federation/epic-stack-remote/tailwind.config.ts
new file mode 100644
index 0000000..0c4695b
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tailwind.config.ts
@@ -0,0 +1,22 @@
+import { type Config } from 'tailwindcss'
+import animatePlugin from 'tailwindcss-animate'
+import radixPlugin from 'tailwindcss-radix'
+import { marketingPreset } from './app/routes/_marketing+/tailwind-preset'
+import { extendedTheme } from './app/utils/extended-theme.ts'
+
+export default {
+ content: ['./app/**/*.{ts,tsx,jsx,js}'],
+ darkMode: 'class',
+ theme: {
+ container: {
+ center: true,
+ padding: '2rem',
+ screens: {
+ '2xl': '1400px',
+ },
+ },
+ extend: extendedTheme,
+ },
+ presets: [marketingPreset],
+ plugins: [animatePlugin, radixPlugin],
+} satisfies Config
diff --git a/examples/federation/epic-stack-remote/template.env b/examples/federation/epic-stack-remote/template.env
new file mode 100644
index 0000000..df848a1
--- /dev/null
+++ b/examples/federation/epic-stack-remote/template.env
@@ -0,0 +1,21 @@
+LITEFS_DIR="/litefs/data"
+DATABASE_PATH="./prisma/data.db"
+DATABASE_URL="file:./data.db?connection_limit=1"
+CACHE_DATABASE_PATH="./other/cache.db"
+SESSION_SECRET="super-duper-s3cret"
+HONEYPOT_SECRET="super-duper-s3cret"
+RESEND_API_KEY="re_blAh_blaHBlaHblahBLAhBlAh"
+SENTRY_DSN="your-dsn"
+
+# this is set to a random value in the Dockerfile
+INTERNAL_COMMAND_TOKEN="some-made-up-token"
+
+# the mocks and some code rely on these two being prefixed with "MOCK_"
+# if they aren't then the real github api will be attempted
+GITHUB_CLIENT_ID="MOCK_GITHUB_CLIENT_ID"
+GITHUB_CLIENT_SECRET="MOCK_GITHUB_CLIENT_SECRET"
+GITHUB_TOKEN="MOCK_GITHUB_TOKEN"
+
+# set this to false to prevent search engines from indexing the website
+# default to allow indexing for seo safety
+ALLOW_INDEXING="true"
diff --git a/examples/federation/epic-stack-remote/tests/db-utils.ts b/examples/federation/epic-stack-remote/tests/db-utils.ts
new file mode 100644
index 0000000..beb412c
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/db-utils.ts
@@ -0,0 +1,115 @@
+import fs from 'node:fs'
+import { faker } from '@faker-js/faker'
+import bcrypt from 'bcryptjs'
+import { UniqueEnforcer } from 'enforce-unique'
+
+const uniqueUsernameEnforcer = new UniqueEnforcer()
+
+export function createUser() {
+ const firstName = faker.person.firstName()
+ const lastName = faker.person.lastName()
+
+ const username = uniqueUsernameEnforcer
+ .enforce(() => {
+ return (
+ faker.string.alphanumeric({ length: 2 }) +
+ '_' +
+ faker.internet.username({
+ firstName: firstName.toLowerCase(),
+ lastName: lastName.toLowerCase(),
+ })
+ )
+ })
+ .slice(0, 20)
+ .toLowerCase()
+ .replace(/[^a-z0-9_]/g, '_')
+ return {
+ username,
+ name: `${firstName} ${lastName}`,
+ email: `${username}@example.com`,
+ }
+}
+
+export function createPassword(password: string = faker.internet.password()) {
+ return {
+ hash: bcrypt.hashSync(password, 10),
+ }
+}
+
+let noteImages: Array>> | undefined
+export async function getNoteImages() {
+ if (noteImages) return noteImages
+
+ noteImages = await Promise.all([
+ img({
+ altText: 'a nice country house',
+ filepath: './tests/fixtures/images/notes/0.png',
+ }),
+ img({
+ altText: 'a city scape',
+ filepath: './tests/fixtures/images/notes/1.png',
+ }),
+ img({
+ altText: 'a sunrise',
+ filepath: './tests/fixtures/images/notes/2.png',
+ }),
+ img({
+ altText: 'a group of friends',
+ filepath: './tests/fixtures/images/notes/3.png',
+ }),
+ img({
+ altText: 'friends being inclusive of someone who looks lonely',
+ filepath: './tests/fixtures/images/notes/4.png',
+ }),
+ img({
+ altText: 'an illustration of a hot air balloon',
+ filepath: './tests/fixtures/images/notes/5.png',
+ }),
+ img({
+ altText:
+ 'an office full of laptops and other office equipment that look like it was abandoned in a rush out of the building in an emergency years ago.',
+ filepath: './tests/fixtures/images/notes/6.png',
+ }),
+ img({
+ altText: 'a rusty lock',
+ filepath: './tests/fixtures/images/notes/7.png',
+ }),
+ img({
+ altText: 'something very happy in nature',
+ filepath: './tests/fixtures/images/notes/8.png',
+ }),
+ img({
+ altText: `someone at the end of a cry session who's starting to feel a little better.`,
+ filepath: './tests/fixtures/images/notes/9.png',
+ }),
+ ])
+
+ return noteImages
+}
+
+let userImages: Array>> | undefined
+export async function getUserImages() {
+ if (userImages) return userImages
+
+ userImages = await Promise.all(
+ Array.from({ length: 10 }, (_, index) =>
+ img({ filepath: `./tests/fixtures/images/user/${index}.jpg` }),
+ ),
+ )
+
+ return userImages
+}
+
+export async function img({
+ altText,
+ filepath,
+}: {
+ altText?: string
+ filepath: string
+}) {
+ return {
+ altText,
+ contentType: filepath.endsWith('.png') ? 'image/png' : 'image/jpeg',
+ blob: await fs.promises.readFile(filepath),
+ }
+}
diff --git a/examples/federation/epic-stack-remote/tests/e2e/2fa.test.ts b/examples/federation/epic-stack-remote/tests/e2e/2fa.test.ts
new file mode 100644
index 0000000..318eab2
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/e2e/2fa.test.ts
@@ -0,0 +1,64 @@
+import { faker } from '@faker-js/faker'
+import { generateTOTP } from '#app/utils/totp.server.ts'
+import { expect, test } from '#tests/playwright-utils.ts'
+
+test('Users can add 2FA to their account and use it when logging in', async ({
+ page,
+ login,
+}) => {
+ const password = faker.internet.password()
+ const user = await login({ password })
+ await page.goto('/settings/profile')
+
+ await page.getByRole('link', { name: /enable 2fa/i }).click()
+
+ await expect(page).toHaveURL(`/settings/profile/two-factor`)
+ const main = page.getByRole('main')
+ await main.getByRole('button', { name: /enable 2fa/i }).click()
+ const otpUriString = await main
+ .getByLabel(/One-Time Password URI/i)
+ .innerText()
+
+ const otpUri = new URL(otpUriString)
+ const options = Object.fromEntries(otpUri.searchParams)
+
+ await main.getByRole('textbox', { name: /code/i }).fill(
+ (
+ await generateTOTP({
+ ...options,
+ // the algorithm will be "SHA1" but we need to generate the OTP with "SHA-1"
+ algorithm: 'SHA-1',
+ })
+ ).otp,
+ )
+ await main.getByRole('button', { name: /submit/i }).click()
+
+ await expect(main).toHaveText(/You have enabled two-factor authentication./i)
+ await expect(main.getByRole('link', { name: /disable 2fa/i })).toBeVisible()
+
+ await page.getByRole('link', { name: user.name ?? user.username }).click()
+ await page.getByRole('menuitem', { name: /logout/i }).click()
+ await expect(page).toHaveURL(`/`)
+
+ await page.goto('/login')
+ await expect(page).toHaveURL(`/login`)
+ await page.getByRole('textbox', { name: /username/i }).fill(user.username)
+ await page.getByLabel(/^password$/i).fill(password)
+ await page.getByRole('button', { name: /log in/i }).click()
+
+ await page.getByRole('textbox', { name: /code/i }).fill(
+ (
+ await generateTOTP({
+ ...options,
+ // the algorithm will be "SHA1" but we need to generate the OTP with "SHA-1"
+ algorithm: 'SHA-1',
+ })
+ ).otp,
+ )
+
+ await page.getByRole('button', { name: /submit/i }).click()
+
+ await expect(
+ page.getByRole('link', { name: user.name ?? user.username }),
+ ).toBeVisible()
+})
diff --git a/examples/federation/epic-stack-remote/tests/e2e/error-boundary.test.ts b/examples/federation/epic-stack-remote/tests/e2e/error-boundary.test.ts
new file mode 100644
index 0000000..5254f0f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/e2e/error-boundary.test.ts
@@ -0,0 +1,9 @@
+import { expect, test } from '#tests/playwright-utils.ts'
+
+test('Test root error boundary caught', async ({ page }) => {
+ const pageUrl = '/does-not-exist'
+ const res = await page.goto(pageUrl)
+
+ expect(res?.status()).toBe(404)
+ await expect(page.getByText(/We can't find this page/i)).toBeVisible()
+})
diff --git a/examples/federation/epic-stack-remote/tests/e2e/note-images.test.ts b/examples/federation/epic-stack-remote/tests/e2e/note-images.test.ts
new file mode 100644
index 0000000..5b1addc
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/e2e/note-images.test.ts
@@ -0,0 +1,140 @@
+import fs from 'node:fs'
+import { faker } from '@faker-js/faker'
+import { type NoteImage, type Note } from '@prisma/client'
+import { prisma } from '#app/utils/db.server.ts'
+import { expect, test } from '#tests/playwright-utils.ts'
+
+test('Users can create note with an image', async ({ page, login }) => {
+ const user = await login()
+ await page.goto(`/users/${user.username}/notes`)
+
+ const newNote = createNote()
+ const altText = 'cute koala'
+ await page.getByRole('link', { name: 'new note' }).click()
+
+ // fill in form and submit
+ await page.getByRole('textbox', { name: 'title' }).fill(newNote.title)
+ await page.getByRole('textbox', { name: 'content' }).fill(newNote.content)
+ await page
+ .getByLabel('image')
+ .nth(0)
+ .setInputFiles('tests/fixtures/images/kody-notes/cute-koala.png')
+ await page.getByRole('textbox', { name: 'alt text' }).fill(altText)
+
+ await page.getByRole('button', { name: 'submit' }).click()
+ await expect(page).toHaveURL(new RegExp(`/users/${user.username}/notes/.*`))
+ await expect(page.getByRole('heading', { name: newNote.title })).toBeVisible()
+ await expect(page.getByAltText(altText)).toBeVisible()
+})
+
+test('Users can create note with multiple images', async ({ page, login }) => {
+ const user = await login()
+ await page.goto(`/users/${user.username}/notes`)
+
+ const newNote = createNote()
+ const altText1 = 'cute koala'
+ const altText2 = 'koala coder'
+ await page.getByRole('link', { name: 'new note' }).click()
+
+ // fill in form and submit
+ await page.getByRole('textbox', { name: 'title' }).fill(newNote.title)
+ await page.getByRole('textbox', { name: 'content' }).fill(newNote.content)
+ await page
+ .getByLabel('image')
+ .nth(0)
+ .setInputFiles('tests/fixtures/images/kody-notes/cute-koala.png')
+ await page.getByLabel('alt text').nth(0).fill(altText1)
+ await page.getByRole('button', { name: 'add image' }).click()
+
+ await page
+ .getByLabel('image')
+ .nth(1)
+ .setInputFiles('tests/fixtures/images/kody-notes/koala-coder.png')
+ await page.getByLabel('alt text').nth(1).fill(altText2)
+
+ await page.getByRole('button', { name: 'submit' }).click()
+ await expect(page).toHaveURL(new RegExp(`/users/${user.username}/notes/.*`))
+ await expect(page.getByRole('heading', { name: newNote.title })).toBeVisible()
+ await expect(page.getByAltText(altText1)).toBeVisible()
+ await expect(page.getByAltText(altText2)).toBeVisible()
+})
+
+test('Users can edit note image', async ({ page, login }) => {
+ const user = await login()
+
+ const note = await prisma.note.create({
+ select: { id: true },
+ data: {
+ ...createNoteWithImage(),
+ ownerId: user.id,
+ },
+ })
+ await page.goto(`/users/${user.username}/notes/${note.id}`)
+
+ // edit the image
+ await page.getByRole('link', { name: 'Edit', exact: true }).click()
+ const updatedImage = {
+ altText: 'koala coder',
+ location: 'tests/fixtures/images/kody-notes/koala-coder.png',
+ }
+ await page.getByLabel('image').nth(0).setInputFiles(updatedImage.location)
+ await page.getByLabel('alt text').nth(0).fill(updatedImage.altText)
+ await page.getByRole('button', { name: 'submit' }).click()
+
+ await expect(page).toHaveURL(`/users/${user.username}/notes/${note.id}`)
+ await expect(page.getByAltText(updatedImage.altText)).toBeVisible()
+})
+
+test('Users can delete note image', async ({ page, login }) => {
+ const user = await login()
+
+ const note = await prisma.note.create({
+ select: { id: true, title: true },
+ data: {
+ ...createNoteWithImage(),
+ ownerId: user.id,
+ },
+ })
+ await page.goto(`/users/${user.username}/notes/${note.id}`)
+
+ await expect(page.getByRole('heading', { name: note.title })).toBeVisible()
+ // find image tags
+ const images = page
+ .getByRole('main')
+ .getByRole('list')
+ .getByRole('listitem')
+ .getByRole('img')
+ const countBefore = await images.count()
+ await page.getByRole('link', { name: 'Edit', exact: true }).click()
+ await page.getByRole('button', { name: 'remove image' }).click()
+ await page.getByRole('button', { name: 'submit' }).click()
+ await expect(page).toHaveURL(`/users/${user.username}/notes/${note.id}`)
+ const countAfter = await images.count()
+ expect(countAfter).toEqual(countBefore - 1)
+})
+
+function createNote() {
+ return {
+ title: faker.lorem.words(3),
+ content: faker.lorem.paragraphs(3),
+ } satisfies Omit
+}
+function createNoteWithImage() {
+ return {
+ ...createNote(),
+ images: {
+ create: {
+ altText: 'cute koala',
+ contentType: 'image/png',
+ blob: fs.readFileSync(
+ 'tests/fixtures/images/kody-notes/cute-koala.png',
+ ),
+ },
+ },
+ } satisfies Omit<
+ Note,
+ 'id' | 'createdAt' | 'updatedAt' | 'type' | 'ownerId'
+ > & {
+ images: { create: Pick }
+ }
+}
diff --git a/examples/federation/epic-stack-remote/tests/e2e/notes.test.ts b/examples/federation/epic-stack-remote/tests/e2e/notes.test.ts
new file mode 100644
index 0000000..23b9259
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/e2e/notes.test.ts
@@ -0,0 +1,74 @@
+import { faker } from '@faker-js/faker'
+import { prisma } from '#app/utils/db.server.ts'
+import { expect, test } from '#tests/playwright-utils.ts'
+
+test('Users can create notes', async ({ page, login }) => {
+ const user = await login()
+ await page.goto(`/users/${user.username}/notes`)
+
+ const newNote = createNote()
+ await page.getByRole('link', { name: /New Note/i }).click()
+
+ // fill in form and submit
+ await page.getByRole('textbox', { name: /title/i }).fill(newNote.title)
+ await page.getByRole('textbox', { name: /content/i }).fill(newNote.content)
+
+ await page.getByRole('button', { name: /submit/i }).click()
+ await expect(page).toHaveURL(new RegExp(`/users/${user.username}/notes/.*`))
+})
+
+test('Users can edit notes', async ({ page, login }) => {
+ const user = await login()
+
+ const note = await prisma.note.create({
+ select: { id: true },
+ data: { ...createNote(), ownerId: user.id },
+ })
+ await page.goto(`/users/${user.username}/notes/${note.id}`)
+
+ // edit the note
+ await page.getByRole('link', { name: 'Edit', exact: true }).click()
+ const updatedNote = createNote()
+ await page.getByRole('textbox', { name: /title/i }).fill(updatedNote.title)
+ await page
+ .getByRole('textbox', { name: /content/i })
+ .fill(updatedNote.content)
+ await page.getByRole('button', { name: /submit/i }).click()
+
+ await expect(page).toHaveURL(`/users/${user.username}/notes/${note.id}`)
+ await expect(
+ page.getByRole('heading', { name: updatedNote.title }),
+ ).toBeVisible()
+})
+
+test('Users can delete notes', async ({ page, login }) => {
+ const user = await login()
+
+ const note = await prisma.note.create({
+ select: { id: true },
+ data: { ...createNote(), ownerId: user.id },
+ })
+ await page.goto(`/users/${user.username}/notes/${note.id}`)
+
+ // find links with href prefix
+ const noteLinks = page
+ .getByRole('main')
+ .getByRole('list')
+ .getByRole('listitem')
+ .getByRole('link')
+ const countBefore = await noteLinks.count()
+ await page.getByRole('button', { name: /delete/i }).click()
+ await expect(
+ page.getByText('Your note has been deleted.', { exact: true }),
+ ).toBeVisible()
+ await expect(page).toHaveURL(`/users/${user.username}/notes`)
+ const countAfter = await noteLinks.count()
+ expect(countAfter).toEqual(countBefore - 1)
+})
+
+function createNote() {
+ return {
+ title: faker.lorem.words(3),
+ content: faker.lorem.paragraphs(3),
+ }
+}
diff --git a/examples/federation/epic-stack-remote/tests/e2e/onboarding.test.ts b/examples/federation/epic-stack-remote/tests/e2e/onboarding.test.ts
new file mode 100644
index 0000000..92064e3
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/e2e/onboarding.test.ts
@@ -0,0 +1,431 @@
+import { invariant } from '@epic-web/invariant'
+import { faker } from '@faker-js/faker'
+import { prisma } from '#app/utils/db.server.ts'
+import {
+ normalizeEmail,
+ normalizeUsername,
+} from '#app/utils/providers/provider'
+import {
+ USERNAME_MAX_LENGTH,
+ USERNAME_MIN_LENGTH,
+} from '#app/utils/user-validation'
+import { readEmail } from '#tests/mocks/utils.ts'
+import { createUser, expect, test as base } from '#tests/playwright-utils.ts'
+
+const URL_REGEX = /(?https?:\/\/[^\s$.?#].[^\s]*)/
+const CODE_REGEX = /Here's your verification code: (?[\d\w]+)/
+function extractUrl(text: string) {
+ const match = text.match(URL_REGEX)
+ return match?.groups?.url
+}
+
+const test = base.extend<{
+ getOnboardingData(): {
+ username: string
+ name: string
+ email: string
+ password: string
+ }
+}>({
+ getOnboardingData: async ({}, use) => {
+ const userData = createUser()
+ await use(() => {
+ const onboardingData = {
+ ...userData,
+ password: faker.internet.password(),
+ }
+ return onboardingData
+ })
+ await prisma.user.deleteMany({ where: { username: userData.username } })
+ },
+})
+
+test('onboarding with link', async ({ page, getOnboardingData }) => {
+ const onboardingData = getOnboardingData()
+
+ await page.goto('/')
+
+ await page.getByRole('link', { name: /log in/i }).click()
+ await expect(page).toHaveURL(`/login`)
+
+ const createAccountLink = page.getByRole('link', {
+ name: /create an account/i,
+ })
+ await createAccountLink.click()
+
+ await expect(page).toHaveURL(`/signup`)
+
+ const emailTextbox = page.getByRole('textbox', { name: /email/i })
+ await emailTextbox.click()
+ await emailTextbox.fill(onboardingData.email)
+
+ await page.getByRole('button', { name: /submit/i }).click()
+ await expect(
+ page.getByRole('button', { name: /submit/i, disabled: true }),
+ ).toBeVisible()
+ await expect(page.getByText(/check your email/i)).toBeVisible()
+
+ const email = await readEmail(onboardingData.email)
+ invariant(email, 'Email not found')
+ expect(email.to).toBe(onboardingData.email.toLowerCase())
+ expect(email.from).toBe('hello@epicstack.dev')
+ expect(email.subject).toMatch(/welcome/i)
+ const onboardingUrl = extractUrl(email.text)
+ invariant(onboardingUrl, 'Onboarding URL not found')
+ await page.goto(onboardingUrl)
+
+ await expect(page).toHaveURL(/\/verify/)
+
+ await page
+ .getByRole('main')
+ .getByRole('button', { name: /submit/i })
+ .click()
+
+ await expect(page).toHaveURL(`/onboarding`)
+ await page
+ .getByRole('textbox', { name: /^username/i })
+ .fill(onboardingData.username)
+
+ await page.getByRole('textbox', { name: /^name/i }).fill(onboardingData.name)
+
+ await page.getByLabel(/^password/i).fill(onboardingData.password)
+
+ await page.getByLabel(/^confirm password/i).fill(onboardingData.password)
+
+ await page.getByLabel(/terms/i).check()
+
+ await page.getByLabel(/remember me/i).check()
+
+ await page.getByRole('button', { name: /Create an account/i }).click()
+
+ await expect(page).toHaveURL(`/`)
+
+ await page.getByRole('link', { name: onboardingData.name }).click()
+ await page.getByRole('menuitem', { name: /profile/i }).click()
+
+ await expect(page).toHaveURL(`/users/${onboardingData.username}`)
+
+ await page.getByRole('link', { name: onboardingData.name }).click()
+ await page.getByRole('menuitem', { name: /logout/i }).click()
+ await expect(page).toHaveURL(`/`)
+})
+
+test('onboarding with a short code', async ({ page, getOnboardingData }) => {
+ const onboardingData = getOnboardingData()
+
+ await page.goto('/signup')
+
+ const emailTextbox = page.getByRole('textbox', { name: /email/i })
+ await emailTextbox.click()
+ await emailTextbox.fill(onboardingData.email)
+
+ await page.getByRole('button', { name: /submit/i }).click()
+ await expect(
+ page.getByRole('button', { name: /submit/i, disabled: true }),
+ ).toBeVisible()
+ await expect(page.getByText(/check your email/i)).toBeVisible()
+
+ const email = await readEmail(onboardingData.email)
+ invariant(email, 'Email not found')
+ expect(email.to).toBe(onboardingData.email.toLowerCase())
+ expect(email.from).toBe('hello@epicstack.dev')
+ expect(email.subject).toMatch(/welcome/i)
+ const codeMatch = email.text.match(CODE_REGEX)
+ const code = codeMatch?.groups?.code
+ invariant(code, 'Onboarding code not found')
+ await page.getByRole('textbox', { name: /code/i }).fill(code)
+ await page.getByRole('button', { name: /submit/i }).click()
+
+ await expect(page).toHaveURL(`/onboarding`)
+})
+
+test('completes onboarding after GitHub OAuth given valid user details', async ({
+ page,
+ prepareGitHubUser,
+}) => {
+ const ghUser = await prepareGitHubUser()
+
+ // let's verify we do not have user with that email in our system:
+ expect(
+ await prisma.user.findUnique({
+ where: { email: normalizeEmail(ghUser.primaryEmail) },
+ }),
+ ).toBeNull()
+
+ await page.goto('/signup')
+ await page.getByRole('button', { name: /signup with github/i }).click()
+
+ await expect(page).toHaveURL(/\/onboarding\/github/)
+ await expect(
+ page.getByText(new RegExp(`welcome aboard ${ghUser.primaryEmail}`, 'i')),
+ ).toBeVisible()
+
+ // fields are pre-populated for the user, so we only need to accept
+ // terms of service and hit the 'crete an account' button
+ const usernameInput = page.getByRole('textbox', { name: /username/i })
+ await expect(usernameInput).toHaveValue(
+ normalizeUsername(ghUser.profile.login),
+ )
+ await expect(page.getByRole('textbox', { name: /^name/i })).toHaveValue(
+ ghUser.profile.name,
+ )
+ const createAccountButton = page.getByRole('button', {
+ name: /create an account/i,
+ })
+
+ await page
+ .getByLabel(/do you agree to our terms of service and privacy policy/i)
+ .check()
+ await createAccountButton.click()
+ await expect(page).toHaveURL(/signup/i)
+
+ // we are still on the 'signup' route since that
+ // was the referrer and no 'redirectTo' has been specified
+ await expect(page).toHaveURL('/signup')
+ await expect(page.getByText(/thanks for signing up/i)).toBeVisible()
+
+ // internally, a user has been created:
+ await prisma.user.findUniqueOrThrow({
+ where: { email: normalizeEmail(ghUser.primaryEmail) },
+ })
+})
+
+test('logs user in after GitHub OAuth if they are already registered', async ({
+ page,
+ prepareGitHubUser,
+}) => {
+ const ghUser = await prepareGitHubUser()
+
+ // let's verify we do not have user with that email in our system ...
+ expect(
+ await prisma.user.findUnique({
+ where: { email: normalizeEmail(ghUser.primaryEmail) },
+ }),
+ ).toBeNull()
+ // ... and create one:
+ const name = faker.person.fullName()
+ const user = await prisma.user.create({
+ select: { id: true, name: true },
+ data: {
+ email: normalizeEmail(ghUser.primaryEmail),
+ username: normalizeUsername(ghUser.profile.login),
+ name,
+ },
+ })
+
+ // let's verify there is no connection between the GitHub user
+ // and out app's user:
+ const connection = await prisma.connection.findFirst({
+ where: { providerName: 'github', userId: user.id },
+ })
+ expect(connection).toBeNull()
+
+ await page.goto('/signup')
+ await page.getByRole('button', { name: /signup with github/i }).click()
+
+ await expect(page).toHaveURL(`/`)
+ await expect(
+ page.getByText(
+ new RegExp(
+ `your "${ghUser!.profile.login}" github account has been connected`,
+ 'i',
+ ),
+ ),
+ ).toBeVisible()
+
+ // internally, a connection (rather than a new user) has been created:
+ await prisma.connection.findFirstOrThrow({
+ where: { providerName: 'github', userId: user.id },
+ })
+})
+
+test('shows help texts on entering invalid details on onboarding page after GitHub OAuth', async ({
+ page,
+ prepareGitHubUser,
+}) => {
+ const ghUser = await prepareGitHubUser()
+
+ await page.goto('/signup')
+ await page.getByRole('button', { name: /signup with github/i }).click()
+
+ await expect(page).toHaveURL(/\/onboarding\/github/)
+ await expect(
+ page.getByText(new RegExp(`welcome aboard ${ghUser.primaryEmail}`, 'i')),
+ ).toBeVisible()
+
+ const usernameInput = page.getByRole('textbox', { name: /username/i })
+
+ // notice, how button is currently in 'idle' (neutral) state and so has got no companion
+ const createAccountButton = page.getByRole('button', {
+ name: /create an account/i,
+ })
+ await expect(createAccountButton.getByRole('status')).not.toBeVisible()
+ await expect(createAccountButton.getByText('error')).not.toBeAttached()
+
+ // invalid chars in username
+ await usernameInput.fill('U$er_name') // $ is invalid char, see app/utils/user-validation.ts.
+ await createAccountButton.click()
+
+ await expect(createAccountButton.getByRole('status')).toBeVisible()
+ await expect(createAccountButton.getByText('error')).toBeAttached()
+ await expect(
+ page.getByText(
+ /username can only include letters, numbers, and underscores/i,
+ ),
+ ).toBeVisible()
+ // but we also never checked that privacy consent box
+ await expect(
+ page.getByText(
+ /you must agree to the terms of service and privacy policy/i,
+ ),
+ ).toBeVisible()
+ await expect(page).toHaveURL(/\/onboarding\/github/)
+
+ // empty username
+ await usernameInput.fill('')
+ await createAccountButton.click()
+ await expect(page.getByText(/username is required/i)).toBeVisible()
+ await expect(page).toHaveURL(/\/onboarding\/github/)
+
+ // too short username
+ await usernameInput.fill(
+ faker.string.alphanumeric({ length: USERNAME_MIN_LENGTH - 1 }),
+ )
+ await createAccountButton.click()
+ await expect(page.getByText(/username is too short/i)).toBeVisible()
+
+ // too long username
+ await usernameInput.fill(
+ faker.string.alphanumeric({
+ length: USERNAME_MAX_LENGTH + 1,
+ }),
+ )
+ // we are truncating the user's input
+ expect((await usernameInput.inputValue()).length).toBe(USERNAME_MAX_LENGTH)
+ await createAccountButton.click()
+ await expect(page.getByText(/username is too long/i)).not.toBeVisible()
+
+ // still unchecked 'terms of service' checkbox
+ await usernameInput.fill(
+ normalizeUsername(`U5er_name_0k_${faker.person.lastName()}`),
+ )
+ await createAccountButton.click()
+ await expect(
+ page.getByText(/must agree to the terms of service and privacy policy/i),
+ ).toBeVisible()
+ await expect(page).toHaveURL(/\/onboarding\/github/)
+
+ // we are all set up and ...
+ await page
+ .getByLabel(/do you agree to our terms of service and privacy policy/i)
+ .check()
+ await createAccountButton.click()
+ await expect(createAccountButton.getByText('error')).not.toBeAttached()
+
+ // ... sign up is successful!
+ await expect(page.getByText(/thanks for signing up/i)).toBeVisible()
+})
+
+test('login as existing user', async ({ page, insertNewUser }) => {
+ const password = faker.internet.password()
+ const user = await insertNewUser({ password })
+ invariant(user.name, 'User name not found')
+ await page.goto('/login')
+ await page.getByRole('textbox', { name: /username/i }).fill(user.username)
+ await page.getByLabel(/^password$/i).fill(password)
+ await page.getByRole('button', { name: /log in/i }).click()
+ await expect(page).toHaveURL(`/`)
+
+ await expect(page.getByRole('link', { name: user.name })).toBeVisible()
+})
+
+test('reset password with a link', async ({ page, insertNewUser }) => {
+ const originalPassword = faker.internet.password()
+ const user = await insertNewUser({ password: originalPassword })
+ invariant(user.name, 'User name not found')
+ await page.goto('/login')
+
+ await page.getByRole('link', { name: /forgot password/i }).click()
+ await expect(page).toHaveURL('/forgot-password')
+
+ await expect(
+ page.getByRole('heading', { name: /forgot password/i }),
+ ).toBeVisible()
+ await page.getByRole('textbox', { name: /username/i }).fill(user.username)
+ await page.getByRole('button', { name: /recover password/i }).click()
+ await expect(
+ page.getByRole('button', { name: /recover password/i, disabled: true }),
+ ).toBeVisible()
+ await expect(page.getByText(/check your email/i)).toBeVisible()
+
+ const email = await readEmail(user.email)
+ invariant(email, 'Email not found')
+ expect(email.subject).toMatch(/password reset/i)
+ expect(email.to).toBe(user.email.toLowerCase())
+ expect(email.from).toBe('hello@epicstack.dev')
+ const resetPasswordUrl = extractUrl(email.text)
+ invariant(resetPasswordUrl, 'Reset password URL not found')
+ await page.goto(resetPasswordUrl)
+
+ await expect(page).toHaveURL(/\/verify/)
+
+ await page
+ .getByRole('main')
+ .getByRole('button', { name: /submit/i })
+ .click()
+
+ await expect(page).toHaveURL(`/reset-password`)
+ const newPassword = faker.internet.password()
+ await page.getByLabel(/^new password$/i).fill(newPassword)
+ await page.getByLabel(/^confirm password$/i).fill(newPassword)
+
+ await page.getByRole('button', { name: /reset password/i }).click()
+ await expect(
+ page.getByRole('button', { name: /reset password/i, disabled: true }),
+ ).toBeVisible()
+
+ await expect(page).toHaveURL('/login')
+ await page.getByRole('textbox', { name: /username/i }).fill(user.username)
+ await page.getByLabel(/^password$/i).fill(originalPassword)
+ await page.getByRole('button', { name: /log in/i }).click()
+
+ await expect(page.getByText(/invalid username or password/i)).toBeVisible()
+
+ await page.getByLabel(/^password$/i).fill(newPassword)
+ await page.getByRole('button', { name: /log in/i }).click()
+
+ await expect(page).toHaveURL(`/`)
+
+ await expect(page.getByRole('link', { name: user.name })).toBeVisible()
+})
+
+test('reset password with a short code', async ({ page, insertNewUser }) => {
+ const user = await insertNewUser()
+ await page.goto('/login')
+
+ await page.getByRole('link', { name: /forgot password/i }).click()
+ await expect(page).toHaveURL('/forgot-password')
+
+ await expect(
+ page.getByRole('heading', { name: /forgot password/i }),
+ ).toBeVisible()
+ await page.getByRole('textbox', { name: /username/i }).fill(user.username)
+ await page.getByRole('button', { name: /recover password/i }).click()
+ await expect(
+ page.getByRole('button', { name: /recover password/i, disabled: true }),
+ ).toBeVisible()
+ await expect(page.getByText(/check your email/i)).toBeVisible()
+
+ const email = await readEmail(user.email)
+ invariant(email, 'Email not found')
+ expect(email.subject).toMatch(/password reset/i)
+ expect(email.to).toBe(user.email)
+ expect(email.from).toBe('hello@epicstack.dev')
+ const codeMatch = email.text.match(CODE_REGEX)
+ const code = codeMatch?.groups?.code
+ invariant(code, 'Reset Password code not found')
+ await page.getByRole('textbox', { name: /code/i }).fill(code)
+ await page.getByRole('button', { name: /submit/i }).click()
+
+ await expect(page).toHaveURL(`/reset-password`)
+})
diff --git a/examples/federation/epic-stack-remote/tests/e2e/search.test.ts b/examples/federation/epic-stack-remote/tests/e2e/search.test.ts
new file mode 100644
index 0000000..270eb4e
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/e2e/search.test.ts
@@ -0,0 +1,26 @@
+import { invariant } from '@epic-web/invariant'
+import { expect, test } from '#tests/playwright-utils.ts'
+
+test('Search from home page', async ({ page, insertNewUser }) => {
+ const newUser = await insertNewUser()
+ await page.goto('/')
+
+ await page.getByRole('searchbox', { name: /search/i }).fill(newUser.username)
+ await page.getByRole('button', { name: /search/i }).click()
+
+ await page.waitForURL(
+ `/users?${new URLSearchParams({ search: newUser.username })}`,
+ )
+ await expect(page.getByText('Epic Notes Users')).toBeVisible()
+ const userList = page.getByRole('main').getByRole('list')
+ await expect(userList.getByRole('listitem')).toHaveCount(1)
+ invariant(newUser.name, 'User name not found')
+ await expect(page.getByAltText(newUser.name)).toBeVisible()
+
+ await page.getByRole('searchbox', { name: /search/i }).fill('__nonexistent__')
+ await page.getByRole('button', { name: /search/i }).click()
+ await page.waitForURL(`/users?search=__nonexistent__`)
+
+ await expect(userList.getByRole('listitem')).not.toBeVisible()
+ await expect(page.getByText(/no users found/i)).toBeVisible()
+})
diff --git a/examples/federation/epic-stack-remote/tests/e2e/settings-profile.test.ts b/examples/federation/epic-stack-remote/tests/e2e/settings-profile.test.ts
new file mode 100644
index 0000000..7af0a08
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/e2e/settings-profile.test.ts
@@ -0,0 +1,115 @@
+import { invariant } from '@epic-web/invariant'
+import { faker } from '@faker-js/faker'
+import { verifyUserPassword } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { readEmail } from '#tests/mocks/utils.ts'
+import { expect, test, createUser, waitFor } from '#tests/playwright-utils.ts'
+
+const CODE_REGEX = /Here's your verification code: (?[\d\w]+)/
+
+test('Users can update their basic info', async ({ page, login }) => {
+ await login()
+ await page.goto('/settings/profile')
+
+ const newUserData = createUser()
+
+ await page.getByRole('textbox', { name: /^name/i }).fill(newUserData.name)
+ await page
+ .getByRole('textbox', { name: /^username/i })
+ .fill(newUserData.username)
+
+ await page.getByRole('button', { name: /^save/i }).click()
+})
+
+test('Users can update their password', async ({ page, login }) => {
+ const oldPassword = faker.internet.password()
+ const newPassword = faker.internet.password()
+ const user = await login({ password: oldPassword })
+ await page.goto('/settings/profile')
+
+ await page.getByRole('link', { name: /change password/i }).click()
+
+ await page
+ .getByRole('textbox', { name: /^current password/i })
+ .fill(oldPassword)
+ await page.getByRole('textbox', { name: /^new password/i }).fill(newPassword)
+ await page
+ .getByRole('textbox', { name: /^confirm new password/i })
+ .fill(newPassword)
+
+ await page.getByRole('button', { name: /^change password/i }).click()
+
+ await expect(page).toHaveURL(`/settings/profile`)
+
+ const { username } = user
+ expect(
+ await verifyUserPassword({ username }, oldPassword),
+ 'Old password still works',
+ ).toEqual(null)
+ expect(
+ await verifyUserPassword({ username }, newPassword),
+ 'New password does not work',
+ ).toEqual({ id: user.id })
+})
+
+test('Users can update their profile photo', async ({ page, login }) => {
+ const user = await login()
+ await page.goto('/settings/profile')
+
+ const beforeSrc = await page
+ .getByRole('img', { name: user.name ?? user.username })
+ .getAttribute('src')
+
+ await page.getByRole('link', { name: /change profile photo/i }).click()
+
+ await expect(page).toHaveURL(`/settings/profile/photo`)
+
+ await page
+ .getByRole('textbox', { name: /change/i })
+ .setInputFiles('./tests/fixtures/images/user/kody.png')
+
+ await page.getByRole('button', { name: /save/i }).click()
+
+ await expect(
+ page,
+ 'Was not redirected after saving the profile photo',
+ ).toHaveURL(`/settings/profile`)
+
+ const afterSrc = await page
+ .getByRole('img', { name: user.name ?? user.username })
+ .getAttribute('src')
+
+ expect(beforeSrc).not.toEqual(afterSrc)
+})
+
+test('Users can change their email address', async ({ page, login }) => {
+ const preUpdateUser = await login()
+ const newEmailAddress = faker.internet.email().toLowerCase()
+ expect(preUpdateUser.email).not.toEqual(newEmailAddress)
+ await page.goto('/settings/profile')
+ await page.getByRole('link', { name: /change email/i }).click()
+ await page.getByRole('textbox', { name: /new email/i }).fill(newEmailAddress)
+ await page.getByRole('button', { name: /send confirmation/i }).click()
+ await expect(page.getByText(/check your email/i)).toBeVisible()
+ const email = await waitFor(() => readEmail(newEmailAddress), {
+ errorMessage: 'Confirmation email was not sent',
+ })
+ invariant(email, 'Email was not sent')
+ const codeMatch = email.text.match(CODE_REGEX)
+ const code = codeMatch?.groups?.code
+ invariant(code, 'Onboarding code not found')
+ await page.getByRole('textbox', { name: /code/i }).fill(code)
+ await page.getByRole('button', { name: /submit/i }).click()
+ await expect(page.getByText(/email changed/i)).toBeVisible()
+
+ const updatedUser = await prisma.user.findUnique({
+ where: { id: preUpdateUser.id },
+ select: { email: true },
+ })
+ invariant(updatedUser, 'Updated user not found')
+ expect(updatedUser.email).toBe(newEmailAddress)
+ const noticeEmail = await waitFor(() => readEmail(preUpdateUser.email), {
+ errorMessage: 'Notice email was not sent',
+ })
+ expect(noticeEmail.subject).toContain('changed')
+})
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/github/ghost.jpg b/examples/federation/epic-stack-remote/tests/fixtures/github/ghost.jpg
new file mode 100644
index 0000000..e5c8bd5
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/github/ghost.jpg differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/cute-koala.png b/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/cute-koala.png
new file mode 100644
index 0000000..b322eb5
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/cute-koala.png differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/koala-coder.png b/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/koala-coder.png
new file mode 100644
index 0000000..63e6d59
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/koala-coder.png differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/koala-cuddle.png b/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/koala-cuddle.png
new file mode 100644
index 0000000..ccc1d3b
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/koala-cuddle.png differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/koala-eating.png b/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/koala-eating.png
new file mode 100644
index 0000000..00713e8
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/koala-eating.png differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/koala-mentor.png b/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/koala-mentor.png
new file mode 100644
index 0000000..9796b8c
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/koala-mentor.png differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/koala-soccer.png b/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/koala-soccer.png
new file mode 100644
index 0000000..9c0e809
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/koala-soccer.png differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/mountain.png b/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/mountain.png
new file mode 100644
index 0000000..115fb9c
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/kody-notes/mountain.png differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/notes/0.png b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/0.png
new file mode 100644
index 0000000..b04ba30
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/0.png differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/notes/1.png b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/1.png
new file mode 100644
index 0000000..307f0a8
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/1.png differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/notes/2.png b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/2.png
new file mode 100644
index 0000000..23b9822
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/2.png differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/notes/3.png b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/3.png
new file mode 100644
index 0000000..ec4e949
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/3.png differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/notes/4.png b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/4.png
new file mode 100644
index 0000000..138a0ad
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/4.png differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/notes/5.png b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/5.png
new file mode 100644
index 0000000..bd2d0c1
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/5.png differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/notes/6.png b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/6.png
new file mode 100644
index 0000000..4d34f94
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/6.png differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/notes/7.png b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/7.png
new file mode 100644
index 0000000..2ccdd83
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/7.png differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/notes/8.png b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/8.png
new file mode 100644
index 0000000..dfb7dab
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/8.png differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/notes/9.png b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/9.png
new file mode 100644
index 0000000..1397419
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/notes/9.png differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/user/0.jpg b/examples/federation/epic-stack-remote/tests/fixtures/images/user/0.jpg
new file mode 100644
index 0000000..d3a9d7a
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/user/0.jpg differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/user/1.jpg b/examples/federation/epic-stack-remote/tests/fixtures/images/user/1.jpg
new file mode 100644
index 0000000..83c858c
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/user/1.jpg differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/user/2.jpg b/examples/federation/epic-stack-remote/tests/fixtures/images/user/2.jpg
new file mode 100644
index 0000000..de2564f
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/user/2.jpg differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/user/3.jpg b/examples/federation/epic-stack-remote/tests/fixtures/images/user/3.jpg
new file mode 100644
index 0000000..0e6a682
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/user/3.jpg differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/user/4.jpg b/examples/federation/epic-stack-remote/tests/fixtures/images/user/4.jpg
new file mode 100644
index 0000000..a9feae7
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/user/4.jpg differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/user/5.jpg b/examples/federation/epic-stack-remote/tests/fixtures/images/user/5.jpg
new file mode 100644
index 0000000..76c1183
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/user/5.jpg differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/user/6.jpg b/examples/federation/epic-stack-remote/tests/fixtures/images/user/6.jpg
new file mode 100644
index 0000000..a5b1e8e
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/user/6.jpg differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/user/7.jpg b/examples/federation/epic-stack-remote/tests/fixtures/images/user/7.jpg
new file mode 100644
index 0000000..70d3eeb
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/user/7.jpg differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/user/8.jpg b/examples/federation/epic-stack-remote/tests/fixtures/images/user/8.jpg
new file mode 100644
index 0000000..82df15f
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/user/8.jpg differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/user/9.jpg b/examples/federation/epic-stack-remote/tests/fixtures/images/user/9.jpg
new file mode 100644
index 0000000..0c61fbc
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/user/9.jpg differ
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/user/README.md b/examples/federation/epic-stack-remote/tests/fixtures/images/user/README.md
new file mode 100644
index 0000000..ecd40c8
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/fixtures/images/user/README.md
@@ -0,0 +1,4 @@
+# User Images
+
+This is used when creating users with images. If you don't do that, feel free to
+delete this directory.
diff --git a/examples/federation/epic-stack-remote/tests/fixtures/images/user/kody.png b/examples/federation/epic-stack-remote/tests/fixtures/images/user/kody.png
new file mode 100644
index 0000000..a10de95
Binary files /dev/null and b/examples/federation/epic-stack-remote/tests/fixtures/images/user/kody.png differ
diff --git a/examples/federation/epic-stack-remote/tests/mocks/README.md b/examples/federation/epic-stack-remote/tests/mocks/README.md
new file mode 100644
index 0000000..b4c3011
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/mocks/README.md
@@ -0,0 +1,9 @@
+# Mocks
+
+Use this to mock any third party HTTP resources that you don't have running
+locally and want to have mocked for local development as well as tests.
+
+Learn more about how to use this at [mswjs.io](https://mswjs.io/)
+
+For an extensive example, see the
+[source code for kentcdodds.com](https://github.com/kentcdodds/kentcdodds.com/blob/main/mocks/index.ts)
diff --git a/examples/federation/epic-stack-remote/tests/mocks/federation.ts b/examples/federation/epic-stack-remote/tests/mocks/federation.ts
new file mode 100644
index 0000000..962f6b0
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/mocks/federation.ts
@@ -0,0 +1,22 @@
+import { HttpResponse, http, type HttpHandler, bypass } from 'msw'
+
+const { text } = HttpResponse
+
+export const handlers: Array = [
+ http.get('*.js', async (req) => {
+ const f = await (await fetch(bypass(req.request.url))).text()
+ return text(f)
+ }),
+ http.get('*.json', async (req) => {
+ const f = await (await fetch(bypass(req.request.url))).text()
+ return text(f)
+ }),
+ http.get('*.zip', async (req) => {
+ const response = await fetch(bypass(req.request.url))
+ const buffer = await response.arrayBuffer()
+ return new Response(buffer, {
+ status: response.status,
+ headers: response.headers
+ })
+ }),
+]
diff --git a/examples/federation/epic-stack-remote/tests/mocks/github.ts b/examples/federation/epic-stack-remote/tests/mocks/github.ts
new file mode 100644
index 0000000..6290a8f
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/mocks/github.ts
@@ -0,0 +1,193 @@
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { faker } from '@faker-js/faker'
+import fsExtra from 'fs-extra'
+import { HttpResponse, passthrough, http, type HttpHandler } from 'msw'
+
+const { json } = HttpResponse
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const here = (...s: Array) => path.join(__dirname, ...s)
+
+const githubUserFixturePath = path.join(
+ here(
+ '..',
+ 'fixtures',
+ 'github',
+ `users.${process.env.VITEST_POOL_ID || 0}.local.json`,
+ ),
+)
+
+await fsExtra.ensureDir(path.dirname(githubUserFixturePath))
+
+function createGitHubUser(code?: string | null) {
+ const createEmail = () => ({
+ email: faker.internet.email(),
+ verified: faker.datatype.boolean(),
+ primary: false, // <-- can only have one of these
+ visibility: faker.helpers.arrayElement(['public', null]),
+ })
+ const primaryEmail = {
+ ...createEmail(),
+ verified: true,
+ primary: true,
+ }
+
+ const emails = [
+ {
+ email: faker.internet.email(),
+ verified: false,
+ primary: false,
+ visibility: 'public',
+ },
+ {
+ email: faker.internet.email(),
+ verified: true,
+ primary: false,
+ visibility: null,
+ },
+ primaryEmail,
+ ]
+
+ code ??= faker.string.uuid()
+ return {
+ code,
+ accessToken: `${code}_mock_access_token`,
+ profile: {
+ login: faker.internet.username(),
+ id: faker.string.uuid(),
+ name: faker.person.fullName(),
+ avatar_url: 'https://github.com/ghost.png',
+ emails: emails.map((e) => e.email),
+ },
+ emails,
+ primaryEmail: primaryEmail.email,
+ }
+}
+
+export type GitHubUser = ReturnType
+
+async function getGitHubUsers() {
+ try {
+ if (await fsExtra.pathExists(githubUserFixturePath)) {
+ const json = await fsExtra.readJson(githubUserFixturePath)
+ return json as Array
+ }
+ return []
+ } catch (error) {
+ console.error(error)
+ return []
+ }
+}
+
+export async function deleteGitHubUser(primaryEmail: string) {
+ const users = await getGitHubUsers()
+ const user = users.find((u) => u.primaryEmail === primaryEmail)
+ if (!user) return null
+ await setGitHubUsers(users.filter((u) => u.primaryEmail !== primaryEmail))
+ return user
+}
+
+export async function deleteGitHubUsers() {
+ await fsExtra.remove(githubUserFixturePath)
+}
+
+async function setGitHubUsers(users: Array) {
+ await fsExtra.writeJson(githubUserFixturePath, users, { spaces: 2 })
+}
+
+export async function insertGitHubUser(code?: string | null) {
+ const githubUsers = await getGitHubUsers()
+ let user = githubUsers.find((u) => u.code === code)
+ if (user) {
+ Object.assign(user, createGitHubUser(code))
+ } else {
+ user = createGitHubUser(code)
+ githubUsers.push(user)
+ }
+ await setGitHubUsers(githubUsers)
+ return user
+}
+
+async function getUser(request: Request) {
+ const accessToken = request.headers
+ .get('authorization')
+ ?.slice('token '.length)
+
+ if (!accessToken) {
+ return new Response('Unauthorized', { status: 401 })
+ }
+ const user = (await getGitHubUsers()).find(
+ (u) => u.accessToken === accessToken,
+ )
+
+ if (!user) {
+ return new Response('Not Found', { status: 404 })
+ }
+ return user
+}
+
+const passthroughGitHub =
+ !process.env.GITHUB_CLIENT_ID.startsWith('MOCK_') &&
+ process.env.NODE_ENV !== 'test'
+
+export const handlers: Array = [
+ http.post(
+ 'https://github.com/login/oauth/access_token',
+ async ({ request }) => {
+ if (passthroughGitHub) return passthrough()
+ const params = new URLSearchParams(await request.text())
+
+ const code = params.get('code')
+ const githubUsers = await getGitHubUsers()
+ let user = githubUsers.find((u) => u.code === code)
+ if (!user) {
+ user = await insertGitHubUser(code)
+ }
+
+ return new Response(
+ new URLSearchParams({
+ access_token: user.accessToken,
+ token_type: '__MOCK_TOKEN_TYPE__',
+ }).toString(),
+ { headers: { 'content-type': 'application/x-www-form-urlencoded' } },
+ )
+ },
+ ),
+ http.get('https://api.github.com/user/emails', async ({ request }) => {
+ if (passthroughGitHub) return passthrough()
+
+ const user = await getUser(request)
+ if (user instanceof Response) return user
+
+ return json(user.emails)
+ }),
+ http.get('https://api.github.com/user/:id', async ({ params }) => {
+ if (passthroughGitHub) return passthrough()
+
+ const mockUser = (await getGitHubUsers()).find(
+ (u) => u.profile.id === params.id,
+ )
+ if (mockUser) return json(mockUser.profile)
+
+ return new Response('Not Found', { status: 404 })
+ }),
+ http.get('https://api.github.com/user', async ({ request }) => {
+ if (passthroughGitHub) return passthrough()
+
+ const user = await getUser(request)
+ if (user instanceof Response) return user
+
+ return json(user.profile)
+ }),
+ http.get('https://github.com/ghost.png', async () => {
+ if (passthroughGitHub) return passthrough()
+
+ const buffer = await fsExtra.readFile('./tests/fixtures/github/ghost.jpg')
+ return new Response(buffer, {
+ // the .png is not a mistake even though it looks like it... It's really a jpg
+ // but the ghost image URL really has a png extension π
+ headers: { 'content-type': 'image/jpg' },
+ })
+ }),
+]
diff --git a/examples/federation/epic-stack-remote/tests/mocks/index.ts b/examples/federation/epic-stack-remote/tests/mocks/index.ts
new file mode 100644
index 0000000..8f1fdb0
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/mocks/index.ts
@@ -0,0 +1,32 @@
+import closeWithGrace from 'close-with-grace'
+import { setupServer } from 'msw/node'
+import { handlers as githubHandlers } from './github.ts'
+import { handlers as resendHandlers } from './resend.ts'
+import { handlers as federationHandlers } from './federation.ts'
+
+export const server = setupServer(...resendHandlers, ...githubHandlers, ...federationHandlers)
+
+server.listen({
+ onUnhandledRequest(request, print) {
+ // Do not print warnings on unhandled requests to https://<:userId>.ingest.us.sentry.io/api/
+ // Note: a request handler with passthrough is not suited with this type of url
+ // until there is a more permissible url catching system
+ // like requested at https://github.com/mswjs/msw/issues/1804
+ if (request.url.includes('.sentry.io')) {
+ return
+ }
+ if(request.url.endsWith('.js') || request.url.endsWith('.zip')) {
+ return;
+ }
+ // Print the regular MSW unhandled request warning otherwise.
+ print.warning()
+ },
+})
+
+if (process.env.NODE_ENV !== 'test') {
+ console.info('πΆ Mock server installed')
+
+ closeWithGrace(() => {
+ server.close()
+ })
+}
diff --git a/examples/federation/epic-stack-remote/tests/mocks/resend.ts b/examples/federation/epic-stack-remote/tests/mocks/resend.ts
new file mode 100644
index 0000000..7e7c424
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/mocks/resend.ts
@@ -0,0 +1,22 @@
+import { faker } from '@faker-js/faker'
+import { HttpResponse, http, type HttpHandler } from 'msw'
+import { requireHeader, writeEmail } from './utils.ts'
+
+const { json } = HttpResponse
+
+export const handlers: Array = [
+ http.post(`https://api.resend.com/emails`, async ({ request }) => {
+ requireHeader(request.headers, 'Authorization')
+ const body = await request.json()
+ console.info('πΆ mocked email contents:', body)
+
+ const email = await writeEmail(body)
+
+ return json({
+ id: faker.string.uuid(),
+ from: email.from,
+ to: email.to,
+ created_at: new Date().toISOString(),
+ })
+ }),
+]
diff --git a/examples/federation/epic-stack-remote/tests/mocks/utils.ts b/examples/federation/epic-stack-remote/tests/mocks/utils.ts
new file mode 100644
index 0000000..0d31af7
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/mocks/utils.ts
@@ -0,0 +1,65 @@
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import fsExtra from 'fs-extra'
+import { z } from 'zod'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const fixturesDirPath = path.join(__dirname, '..', 'fixtures')
+
+export async function readFixture(subdir: string, name: string) {
+ return fsExtra.readJSON(path.join(fixturesDirPath, subdir, `${name}.json`))
+}
+
+export async function createFixture(
+ subdir: string,
+ name: string,
+ data: unknown,
+) {
+ const dir = path.join(fixturesDirPath, subdir)
+ await fsExtra.ensureDir(dir)
+ return fsExtra.writeJSON(path.join(dir, `./${name}.json`), data)
+}
+
+export const EmailSchema = z.object({
+ to: z.string(),
+ from: z.string(),
+ subject: z.string(),
+ text: z.string(),
+ html: z.string(),
+})
+
+export async function writeEmail(rawEmail: unknown) {
+ const email = EmailSchema.parse(rawEmail)
+ await createFixture('email', email.to, email)
+ return email
+}
+
+export async function requireEmail(recipient: string) {
+ const email = await readEmail(recipient)
+ if (!email) throw new Error(`Email to ${recipient} not found`)
+ return email
+}
+
+export async function readEmail(recipient: string) {
+ try {
+ const email = await readFixture('email', recipient)
+ return EmailSchema.parse(email)
+ } catch (error) {
+ console.error(`Error reading email`, error)
+ return null
+ }
+}
+
+export function requireHeader(headers: Headers, header: string) {
+ if (!headers.has(header)) {
+ const headersString = JSON.stringify(
+ Object.fromEntries(headers.entries()),
+ null,
+ 2,
+ )
+ throw new Error(
+ `Header "${header}" required, but not found in ${headersString}`,
+ )
+ }
+ return headers.get(header)
+}
diff --git a/examples/federation/epic-stack-remote/tests/playwright-utils.ts b/examples/federation/epic-stack-remote/tests/playwright-utils.ts
new file mode 100644
index 0000000..a27c02c
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/playwright-utils.ts
@@ -0,0 +1,162 @@
+import { test as base } from '@playwright/test'
+import { type User as UserModel } from '@prisma/client'
+import * as setCookieParser from 'set-cookie-parser'
+import {
+ getPasswordHash,
+ getSessionExpirationDate,
+ sessionKey,
+} from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { MOCK_CODE_GITHUB_HEADER } from '#app/utils/providers/constants.js'
+import { normalizeEmail } from '#app/utils/providers/provider.js'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { createUser } from './db-utils.ts'
+import {
+ type GitHubUser,
+ deleteGitHubUser,
+ insertGitHubUser,
+} from './mocks/github.ts'
+
+export * from './db-utils.ts'
+
+type GetOrInsertUserOptions = {
+ id?: string
+ username?: UserModel['username']
+ password?: string
+ email?: UserModel['email']
+}
+
+type User = {
+ id: string
+ email: string
+ username: string
+ name: string | null
+}
+
+async function getOrInsertUser({
+ id,
+ username,
+ password,
+ email,
+}: GetOrInsertUserOptions = {}): Promise {
+ const select = { id: true, email: true, username: true, name: true }
+ if (id) {
+ return await prisma.user.findUniqueOrThrow({
+ select,
+ where: { id: id },
+ })
+ } else {
+ const userData = createUser()
+ username ??= userData.username
+ password ??= userData.username
+ email ??= userData.email
+ return await prisma.user.create({
+ select,
+ data: {
+ ...userData,
+ email,
+ username,
+ roles: { connect: { name: 'user' } },
+ password: { create: { hash: await getPasswordHash(password) } },
+ },
+ })
+ }
+}
+
+export const test = base.extend<{
+ insertNewUser(options?: GetOrInsertUserOptions): Promise
+ login(options?: GetOrInsertUserOptions): Promise
+ prepareGitHubUser(): Promise
+}>({
+ insertNewUser: async ({}, use) => {
+ let userId: string | undefined = undefined
+ await use(async (options) => {
+ const user = await getOrInsertUser(options)
+ userId = user.id
+ return user
+ })
+ await prisma.user.delete({ where: { id: userId } }).catch(() => {})
+ },
+ login: async ({ page }, use) => {
+ let userId: string | undefined = undefined
+ await use(async (options) => {
+ const user = await getOrInsertUser(options)
+ userId = user.id
+ const session = await prisma.session.create({
+ data: {
+ expirationDate: getSessionExpirationDate(),
+ userId: user.id,
+ },
+ select: { id: true },
+ })
+
+ const authSession = await authSessionStorage.getSession()
+ authSession.set(sessionKey, session.id)
+ const cookieConfig = setCookieParser.parseString(
+ await authSessionStorage.commitSession(authSession),
+ )
+ const newConfig = {
+ ...cookieConfig,
+ domain: 'localhost',
+ expires: cookieConfig.expires?.getTime(),
+ sameSite: cookieConfig.sameSite as 'Strict' | 'Lax' | 'None',
+ }
+ await page.context().addCookies([newConfig])
+ return user
+ })
+ await prisma.user.deleteMany({ where: { id: userId } })
+ },
+ prepareGitHubUser: async ({ page }, use, testInfo) => {
+ await page.route(/\/auth\/github(?!\/callback)/, async (route, request) => {
+ const headers = {
+ ...request.headers(),
+ [MOCK_CODE_GITHUB_HEADER]: testInfo.testId,
+ }
+ await route.continue({ headers })
+ })
+
+ let ghUser: GitHubUser | null = null
+ await use(async () => {
+ const newGitHubUser = await insertGitHubUser(testInfo.testId)!
+ ghUser = newGitHubUser
+ return newGitHubUser
+ })
+
+ const user = await prisma.user.findUniqueOrThrow({
+ select: { id: true, name: true },
+ where: { email: normalizeEmail(ghUser!.primaryEmail) },
+ })
+ await prisma.user.delete({ where: { id: user.id } })
+ await prisma.session.deleteMany({ where: { userId: user.id } })
+ await deleteGitHubUser(ghUser!.primaryEmail)
+ },
+})
+export const { expect } = test
+
+/**
+ * This allows you to wait for something (like an email to be available).
+ *
+ * It calls the callback every 50ms until it returns a value (and does not throw
+ * an error). After the timeout, it will throw the last error that was thrown or
+ * throw the error message provided as a fallback
+ */
+export async function waitFor(
+ cb: () => ReturnValue | Promise,
+ {
+ errorMessage,
+ timeout = 5000,
+ }: { errorMessage?: string; timeout?: number } = {},
+) {
+ const endTime = Date.now() + timeout
+ let lastError: unknown = new Error(errorMessage)
+ while (Date.now() < endTime) {
+ try {
+ const response = await cb()
+ if (response) return response
+ } catch (e: unknown) {
+ lastError = e
+ }
+ await new Promise((r) => setTimeout(r, 100))
+ }
+ throw lastError
+}
diff --git a/examples/federation/epic-stack-remote/tests/setup/custom-matchers.ts b/examples/federation/epic-stack-remote/tests/setup/custom-matchers.ts
new file mode 100644
index 0000000..6e09a20
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/setup/custom-matchers.ts
@@ -0,0 +1,169 @@
+import * as setCookieParser from 'set-cookie-parser'
+import { expect } from 'vitest'
+import { sessionKey } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import {
+ type ToastInput,
+ toastSessionStorage,
+ toastKey,
+} from '#app/utils/toast.server.ts'
+import { convertSetCookieToCookie } from '#tests/utils.ts'
+
+import '@testing-library/jest-dom/vitest'
+
+expect.extend({
+ toHaveRedirect(response: unknown, redirectTo?: string) {
+ if (!(response instanceof Response)) {
+ throw new Error('toHaveRedirect must be called with a Response')
+ }
+ const location = response.headers.get('location')
+ const redirectToSupplied = redirectTo !== undefined
+ if (redirectToSupplied !== Boolean(location)) {
+ return {
+ pass: Boolean(location),
+ message: () =>
+ `Expected response to ${this.isNot ? 'not ' : ''}redirect${
+ redirectToSupplied
+ ? ` to ${this.utils.printExpected(redirectTo)}`
+ : ''
+ } but got ${
+ location ? 'no redirect' : this.utils.printReceived(location)
+ }`,
+ }
+ }
+ const isRedirectStatusCode = response.status >= 300 && response.status < 400
+ if (!isRedirectStatusCode) {
+ return {
+ pass: false,
+ message: () =>
+ `Expected redirect to ${
+ this.isNot ? 'not ' : ''
+ }be ${this.utils.printExpected(
+ '>= 300 && < 400',
+ )} but got ${this.utils.printReceived(response.status)}`,
+ }
+ }
+
+ function toUrl(s?: string | null) {
+ s ??= ''
+ return s.startsWith('http')
+ ? new URL(s)
+ : new URL(s, 'https://example.com')
+ }
+
+ function urlsMatch(u1: URL, u2: URL) {
+ const u1SP = new URL(u1).searchParams
+ u1SP.sort()
+ const u2SP = new URL(u2).searchParams
+ u2SP.sort()
+ return (
+ u1.origin === u2.origin &&
+ u1.pathname === u2.pathname &&
+ u1SP.toString() === u2SP.toString() &&
+ u1.hash === u2.hash
+ )
+ }
+
+ return {
+ pass:
+ location == redirectTo || urlsMatch(toUrl(location), toUrl(redirectTo)),
+ message: () =>
+ `Expected response to ${
+ this.isNot ? 'not ' : ''
+ }redirect to ${this.utils.printExpected(
+ redirectTo,
+ )} but got ${this.utils.printReceived(location)}`,
+ }
+ },
+ async toHaveSessionForUser(response: Response, userId: string) {
+ const setCookies = response.headers.getSetCookie()
+ const sessionSetCookie = setCookies.find(
+ (c) => setCookieParser.parseString(c).name === 'en_session',
+ )
+
+ if (!sessionSetCookie) {
+ return {
+ pass: false,
+ message: () =>
+ `The en_session set-cookie header was${
+ this.isNot ? '' : ' not'
+ } defined`,
+ }
+ }
+
+ const authSession = await authSessionStorage.getSession(
+ convertSetCookieToCookie(sessionSetCookie),
+ )
+ const sessionValue = authSession.get(sessionKey)
+
+ if (!sessionValue) {
+ return {
+ pass: false,
+ message: () => `A session was${this.isNot ? '' : ' not'} set in cookie`,
+ }
+ }
+
+ const session = await prisma.session.findUnique({
+ select: { id: true },
+ where: { userId, id: sessionValue },
+ })
+
+ return {
+ pass: Boolean(session),
+ message: () =>
+ `A session was${
+ this.isNot ? ' not' : ''
+ } created in the database for ${userId}`,
+ }
+ },
+ async toSendToast(response: Response, toast: ToastInput) {
+ const setCookies = response.headers.getSetCookie()
+ const toastSetCookie = setCookies.find(
+ (c) => setCookieParser.parseString(c).name === 'en_toast',
+ )
+
+ if (!toastSetCookie) {
+ return {
+ pass: false,
+ message: () =>
+ `en_toast set-cookie header was${this.isNot ? '' : ' not'} defined`,
+ }
+ }
+
+ const toastSession = await toastSessionStorage.getSession(
+ convertSetCookieToCookie(toastSetCookie),
+ )
+ const toastValue = toastSession.get(toastKey)
+
+ if (!toastValue) {
+ return {
+ pass: false,
+ message: () => `toast was${this.isNot ? '' : ' not'} set in session`,
+ }
+ }
+
+ const pass = this.equals(toastValue, toast)
+
+ const diff = pass ? null : `\n${this.utils.diff(toastValue, toast)}`
+
+ return {
+ pass,
+ message: () =>
+ `toast in the response ${
+ this.isNot ? 'does not match' : 'matches'
+ } the expected toast${diff}`,
+ }
+ },
+})
+
+interface CustomMatchers {
+ toHaveRedirect(redirectTo: string | null): R
+ toHaveSessionForUser(userId: string): Promise
+ toSendToast(toast: ToastInput): Promise
+}
+
+declare module 'vitest' {
+ interface Assertion extends CustomMatchers {}
+ interface AsymmetricMatchersContaining extends CustomMatchers {}
+}
diff --git a/examples/federation/epic-stack-remote/tests/setup/db-setup.ts b/examples/federation/epic-stack-remote/tests/setup/db-setup.ts
new file mode 100644
index 0000000..2cbc646
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/setup/db-setup.ts
@@ -0,0 +1,20 @@
+import path from 'node:path'
+import fsExtra from 'fs-extra'
+import { afterAll, beforeEach } from 'vitest'
+import { BASE_DATABASE_PATH } from './global-setup.ts'
+
+const databaseFile = `./tests/prisma/data.${process.env.VITEST_POOL_ID || 0}.db`
+const databasePath = path.join(process.cwd(), databaseFile)
+process.env.DATABASE_URL = `file:${databasePath}`
+
+beforeEach(async () => {
+ await fsExtra.copyFile(BASE_DATABASE_PATH, databasePath)
+})
+
+afterAll(async () => {
+ // we *must* use dynamic imports here so the process.env.DATABASE_URL is set
+ // before prisma is imported and initialized
+ const { prisma } = await import('#app/utils/db.server.ts')
+ await prisma.$disconnect()
+ await fsExtra.remove(databasePath)
+})
diff --git a/examples/federation/epic-stack-remote/tests/setup/global-setup.ts b/examples/federation/epic-stack-remote/tests/setup/global-setup.ts
new file mode 100644
index 0000000..af7fe31
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/setup/global-setup.ts
@@ -0,0 +1,35 @@
+import path from 'node:path'
+import { execaCommand } from 'execa'
+import fsExtra from 'fs-extra'
+
+export const BASE_DATABASE_PATH = path.join(
+ process.cwd(),
+ `./tests/prisma/base.db`,
+)
+
+export async function setup() {
+ const databaseExists = await fsExtra.pathExists(BASE_DATABASE_PATH)
+
+ if (databaseExists) {
+ const databaseLastModifiedAt = (await fsExtra.stat(BASE_DATABASE_PATH))
+ .mtime
+ const prismaSchemaLastModifiedAt = (
+ await fsExtra.stat('./prisma/schema.prisma')
+ ).mtime
+
+ if (prismaSchemaLastModifiedAt < databaseLastModifiedAt) {
+ return
+ }
+ }
+
+ await execaCommand(
+ 'npx prisma migrate reset --force --skip-seed --skip-generate',
+ {
+ stdio: 'inherit',
+ env: {
+ ...process.env,
+ DATABASE_URL: `file:${BASE_DATABASE_PATH}`,
+ },
+ },
+ )
+}
diff --git a/examples/federation/epic-stack-remote/tests/setup/setup-test-env.ts b/examples/federation/epic-stack-remote/tests/setup/setup-test-env.ts
new file mode 100644
index 0000000..18e2066
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/setup/setup-test-env.ts
@@ -0,0 +1,27 @@
+import 'dotenv/config'
+import './db-setup.ts'
+import '#app/utils/env.server.ts'
+// we need these to be imported first π
+
+import { cleanup } from '@testing-library/react'
+import { afterEach, beforeEach, vi, type MockInstance } from 'vitest'
+import { server } from '#tests/mocks/index.ts'
+import './custom-matchers.ts'
+
+afterEach(() => server.resetHandlers())
+afterEach(() => cleanup())
+
+export let consoleError: MockInstance<(typeof console)['error']>
+
+beforeEach(() => {
+ const originalConsoleError = console.error
+ consoleError = vi.spyOn(console, 'error')
+ consoleError.mockImplementation(
+ (...args: Parameters) => {
+ originalConsoleError(...args)
+ throw new Error(
+ 'Console error was called. Call consoleError.mockImplementation(() => {}) if this is expected.',
+ )
+ },
+ )
+})
diff --git a/examples/federation/epic-stack-remote/tests/utils.ts b/examples/federation/epic-stack-remote/tests/utils.ts
new file mode 100644
index 0000000..b39457d
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tests/utils.ts
@@ -0,0 +1,33 @@
+import * as setCookieParser from 'set-cookie-parser'
+import { sessionKey } from '#app/utils/auth.server.ts'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+
+export const BASE_URL = 'https://www.epicstack.dev'
+
+export function convertSetCookieToCookie(setCookie: string) {
+ const parsedCookie = setCookieParser.parseString(setCookie)
+ return new URLSearchParams({
+ [parsedCookie.name]: parsedCookie.value,
+ }).toString()
+}
+
+export async function getSessionSetCookieHeader(
+ session: { id: string },
+ existingCookie?: string,
+) {
+ const authSession = await authSessionStorage.getSession(existingCookie)
+ authSession.set(sessionKey, session.id)
+ const setCookieHeader = await authSessionStorage.commitSession(authSession)
+ return setCookieHeader
+}
+
+export async function getSessionCookieHeader(
+ session: { id: string },
+ existingCookie?: string,
+) {
+ const setCookieHeader = await getSessionSetCookieHeader(
+ session,
+ existingCookie,
+ )
+ return convertSetCookieToCookie(setCookieHeader)
+}
diff --git a/examples/federation/epic-stack-remote/tsconfig.json b/examples/federation/epic-stack-remote/tsconfig.json
new file mode 100644
index 0000000..94273a2
--- /dev/null
+++ b/examples/federation/epic-stack-remote/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "include": ["**/*.ts", "**/*.tsx", ".react-router/types/**/*"],
+ "extends": ["@epic-web/config/typescript"],
+ "compilerOptions": {
+ // TODO: Probably should move this into epic-web/config
+ "types": ["@react-router/node"],
+ "rootDirs": [".", "./.react-router/types"],
+ "paths": {
+ "#app/*": ["./app/*"],
+ "#tests/*": ["./tests/*"],
+ "@/icon-name": [
+ "./app/components/ui/icons/name.d.ts",
+ "./types/icon-name.d.ts"
+ ]
+ }
+ }
+}
diff --git a/examples/federation/epic-stack-remote/types/deps.d.ts b/examples/federation/epic-stack-remote/types/deps.d.ts
new file mode 100644
index 0000000..121be70
--- /dev/null
+++ b/examples/federation/epic-stack-remote/types/deps.d.ts
@@ -0,0 +1,6 @@
+// This module should contain type definitions for modules which do not have
+// their own type definitions and are not available on DefinitelyTyped.
+
+// declare module 'some-untyped-pkg' {
+// export function foo(): void;
+// }
diff --git a/examples/federation/epic-stack-remote/types/env.env.d.ts b/examples/federation/epic-stack-remote/types/env.env.d.ts
new file mode 100644
index 0000000..ae49594
--- /dev/null
+++ b/examples/federation/epic-stack-remote/types/env.env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/federation/epic-stack-remote/types/icon-name.d.ts b/examples/federation/epic-stack-remote/types/icon-name.d.ts
new file mode 100644
index 0000000..6f47700
--- /dev/null
+++ b/examples/federation/epic-stack-remote/types/icon-name.d.ts
@@ -0,0 +1,3 @@
+// This file is a fallback until you run npm run build:icons
+
+export type IconName = string
diff --git a/examples/federation/epic-stack-remote/types/reset.d.ts b/examples/federation/epic-stack-remote/types/reset.d.ts
new file mode 100644
index 0000000..fa7fab4
--- /dev/null
+++ b/examples/federation/epic-stack-remote/types/reset.d.ts
@@ -0,0 +1,2 @@
+// Do not add any other lines of code to this file!
+import '@epic-web/config/reset.d.ts'
diff --git a/examples/federation/epic-stack/.github/PULL_REQUEST_TEMPLATE.md b/examples/federation/epic-stack/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..84a2084
--- /dev/null
+++ b/examples/federation/epic-stack/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,15 @@
+
+
+## Test Plan
+
+
+
+## Checklist
+
+- [ ] Tests updated
+- [ ] Docs updated
+
+## Screenshots
+
+
diff --git a/examples/federation/epic-stack/.github/workflows/deploy.yml b/examples/federation/epic-stack/.github/workflows/deploy.yml
new file mode 100644
index 0000000..e0eac2b
--- /dev/null
+++ b/examples/federation/epic-stack/.github/workflows/deploy.yml
@@ -0,0 +1,174 @@
+name: π Deploy
+on:
+ push:
+ branches:
+ - main
+ - dev
+ pull_request: {}
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ actions: write
+ contents: read
+
+jobs:
+ lint:
+ name: ⬣ ESLint
+ runs-on: ubuntu-22.04
+ steps:
+ - name: β¬οΈ Checkout repo
+ uses: actions/checkout@v4
+
+ - name: β Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: π₯ Download deps
+ uses: bahmutov/npm-install@v1
+
+ - name: πΌ Build icons
+ run: npm run build:icons
+
+ - name: π¬ Lint
+ run: npm run lint
+
+ typecheck:
+ name: Κ¦ TypeScript
+ runs-on: ubuntu-22.04
+ steps:
+ - name: β¬οΈ Checkout repo
+ uses: actions/checkout@v4
+
+ - name: β Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: π₯ Download deps
+ uses: bahmutov/npm-install@v1
+
+ - name: πΌ Build icons
+ run: npm run build:icons
+
+ - name: π Type check
+ run: npm run typecheck --if-present
+
+ vitest:
+ name: β‘ Vitest
+ runs-on: ubuntu-22.04
+ steps:
+ - name: β¬οΈ Checkout repo
+ uses: actions/checkout@v4
+
+ - name: β Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: π₯ Download deps
+ uses: bahmutov/npm-install@v1
+
+ - name: π Copy test env vars
+ run: cp .env.example .env
+
+ - name: πΌ Build icons
+ run: npm run build:icons
+
+ - name: β‘ Run vitest
+ run: npm run test -- --coverage
+
+ playwright:
+ name: π Playwright
+ runs-on: ubuntu-22.04
+ timeout-minutes: 60
+ steps:
+ - name: β¬οΈ Checkout repo
+ uses: actions/checkout@v4
+
+ - name: π Copy test env vars
+ run: cp .env.example .env
+
+ - name: β Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: π₯ Download deps
+ uses: bahmutov/npm-install@v1
+
+ - name: π₯ Install Playwright Browsers
+ run: npm run test:e2e:install
+
+ - name: π Setup Database
+ run: npx prisma migrate deploy
+
+ - name: π¦ Cache Database
+ id: db-cache
+ uses: actions/cache@v4
+ with:
+ path: prisma/data.db
+ key:
+ db-cache-schema_${{ hashFiles('./prisma/schema.prisma')
+ }}-migrations_${{ hashFiles('./prisma/migrations/*/migration.sql')
+ }}
+
+ - name: π± Seed Database
+ if: steps.db-cache.outputs.cache-hit != 'true'
+ run: npx prisma migrate reset --force
+
+ - name: π Build
+ run: npm run build
+
+ - name: π Playwright tests
+ run: npx playwright test
+
+ - name: π Upload report
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 30
+
+ deploy:
+ name: π Deploy
+ runs-on: ubuntu-22.04
+ needs: [lint, typecheck, vitest, playwright]
+ # only build/deploy branches on pushes
+ if: ${{ github.event_name == 'push' }}
+
+ steps:
+ - name: β¬οΈ Checkout repo
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: '50'
+
+ - name: π Read app name
+ uses: SebRollen/toml-action@v1.2.0
+ id: app_name
+ with:
+ file: 'fly.toml'
+ field: 'app'
+
+ - name: π Setup Fly
+ uses: superfly/flyctl-actions/setup-flyctl@1.5
+
+ - name: π Deploy Staging
+ if: ${{ github.ref == 'refs/heads/dev' }}
+ run:
+ flyctl deploy --remote-only --build-arg COMMIT_SHA=${{ github.sha }}
+ --app ${{ steps.app_name.outputs.value }}-staging
+ env:
+ FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
+
+ - name: π Deploy Production
+ if: ${{ github.ref == 'refs/heads/main' }}
+ run:
+ flyctl deploy --remote-only --build-arg COMMIT_SHA=${{ github.sha }}
+ --build-secret SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
+ env:
+ FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
diff --git a/examples/federation/epic-stack/.github/workflows/version.yml b/examples/federation/epic-stack/.github/workflows/version.yml
new file mode 100644
index 0000000..1a7515d
--- /dev/null
+++ b/examples/federation/epic-stack/.github/workflows/version.yml
@@ -0,0 +1,54 @@
+name: π Version
+on:
+ push:
+ branches:
+ - main
+ - dev
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: write
+
+jobs:
+ version:
+ name: π Update Version
+ runs-on: ubuntu-22.04
+ if: ${{ github.event_name == 'push' }}
+
+ steps:
+ - name: β¬οΈ Checkout repo
+ uses: actions/checkout@v4
+
+ - name: π’ Get HEAD commit hash
+ id: get_head_hash
+ run: echo "HEAD_HASH=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
+
+ - name: π
Get current date
+ id: get_date
+ run:
+ echo "CURRENT_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
+
+ - name: π Update package.json
+ run: |
+ jq '
+ if .["epic-stack"] then
+ .["epic-stack"].head = "${{ steps.get_head_hash.outputs.HEAD_HASH }}" |
+ .["epic-stack"].date = "${{ steps.get_date.outputs.CURRENT_DATE }}"
+ else
+ .["epic-stack"] = {
+ "head": "${{ steps.get_head_hash.outputs.HEAD_HASH }}",
+ "date": "${{ steps.get_date.outputs.CURRENT_DATE }}"
+ }
+ end
+ ' package.json > temp.json && mv temp.json package.json
+
+ - name: πΎ Commit changes
+ run: |
+ git config --local user.email "kody@epicweb.dev"
+ git config --local user.name "kody"
+ git add package.json
+ git commit -m "Update epic-stack version [skip ci]"
+ git push
diff --git a/examples/federation/epic-stack/.gitignore b/examples/federation/epic-stack/.gitignore
new file mode 100644
index 0000000..6ba5a4f
--- /dev/null
+++ b/examples/federation/epic-stack/.gitignore
@@ -0,0 +1,26 @@
+node_modules
+.DS_store
+
+/build
+/server-build
+.env
+.cache
+
+/prisma/data.db
+/prisma/data.db-journal
+/tests/prisma
+
+/test-results/
+/playwright-report/
+/playwright/.cache/
+/tests/fixtures/email/
+/coverage
+
+/other/cache.db
+
+# Easy way to create temporary files/folders that won't accidentally be added to git
+*.local.*
+
+# generated files
+/app/components/ui/icons
+.react-router/
diff --git a/examples/federation/epic-stack/.prettierignore b/examples/federation/epic-stack/.prettierignore
new file mode 100644
index 0000000..f022d02
--- /dev/null
+++ b/examples/federation/epic-stack/.prettierignore
@@ -0,0 +1,15 @@
+node_modules
+
+/build
+/public/build
+/server-build
+.env
+
+/test-results/
+/playwright-report/
+/playwright/.cache/
+/tests/fixtures/email/*.json
+/coverage
+/prisma/migrations
+
+package-lock.json
diff --git a/examples/federation/epic-stack/.vscode/extensions.json b/examples/federation/epic-stack/.vscode/extensions.json
new file mode 100644
index 0000000..3c0a690
--- /dev/null
+++ b/examples/federation/epic-stack/.vscode/extensions.json
@@ -0,0 +1,11 @@
+{
+ "recommendations": [
+ "bradlc.vscode-tailwindcss",
+ "dbaeumer.vscode-eslint",
+ "esbenp.prettier-vscode",
+ "prisma.prisma",
+ "qwtel.sqlite-viewer",
+ "yoavbls.pretty-ts-errors",
+ "github.vscode-github-actions"
+ ]
+}
diff --git a/examples/federation/epic-stack/.vscode/remix.code-snippets b/examples/federation/epic-stack/.vscode/remix.code-snippets
new file mode 100644
index 0000000..39e959f
--- /dev/null
+++ b/examples/federation/epic-stack/.vscode/remix.code-snippets
@@ -0,0 +1,80 @@
+{
+ "loader": {
+ "prefix": "/loader",
+ "scope": "typescriptreact,javascriptreact,typescript,javascript",
+ "body": [
+ "import { type Route } from \"./+types/${TM_FILENAME_BASE}.ts\"",
+ "",
+ "export async function loader({ request }: Route.LoaderArgs) {",
+ " return {}",
+ "}",
+ ],
+ },
+ "action": {
+ "prefix": "/action",
+ "scope": "typescriptreact,javascriptreact,typescript,javascript",
+ "body": [
+ "import { type Route } from \"./+types/${TM_FILENAME_BASE}.ts\"",
+ "",
+ "export async function action({ request }: Route.ActionArgs) {",
+ " return {}",
+ "}",
+ ],
+ },
+ "default": {
+ "prefix": "/default",
+ "scope": "typescriptreact,javascriptreact,typescript,javascript",
+ "body": [
+ "export default function ${TM_FILENAME_BASE/[^a-zA-Z0-9]*([a-zA-Z0-9])([a-zA-Z0-9]*)/${1:/capitalize}${2}/g}() {",
+ " return (",
+ " ",
+ "
Unknown Route ",
+ " ",
+ " )",
+ "}",
+ ],
+ },
+ "headers": {
+ "prefix": "/headers",
+ "scope": "typescriptreact,javascriptreact,typescript,javascript",
+ "body": [
+ "import { type Route } from \"./+types/${TM_FILENAME_BASE}.ts\"",
+ "export const headers: Route.HeadersFunction = ({ loaderHeaders }) => ({",
+ " 'Cache-Control': loaderHeaders.get('Cache-Control') ?? '',",
+ "})",
+ ],
+ },
+ "links": {
+ "prefix": "/links",
+ "scope": "typescriptreact,javascriptreact,typescript,javascript",
+ "body": [
+ "import { type Route } from \"./+types/${TM_FILENAME_BASE}.ts\"",
+ "",
+ "export const links: Route.LinksFunction = () => {",
+ " return []",
+ "}",
+ ],
+ },
+ "meta": {
+ "prefix": "/meta",
+ "scope": "typescriptreact,javascriptreact,typescript,javascript",
+ "body": [
+ "import { type Route } from \"./+types/${TM_FILENAME_BASE}.ts\"",
+ "",
+ "export const meta: Route.MetaFunction = ({ data }) => [{",
+ " title: 'Title',",
+ "}]",
+ ],
+ },
+ "shouldRevalidate": {
+ "prefix": "/shouldRevalidate",
+ "scope": "typescriptreact,javascriptreact,typescript,javascript",
+ "body": [
+ "import { type ShouldRevalidateFunctionArgs } from 'react-router'",
+ "",
+ "export function shouldRevalidate({ defaultShouldRevalidate }: ShouldRevalidateFunctionArgs) {",
+ " return defaultShouldRevalidate",
+ "}",
+ ],
+ },
+}
\ No newline at end of file
diff --git a/examples/federation/epic-stack/.vscode/settings.json b/examples/federation/epic-stack/.vscode/settings.json
new file mode 100644
index 0000000..9ec5cad
--- /dev/null
+++ b/examples/federation/epic-stack/.vscode/settings.json
@@ -0,0 +1,14 @@
+{
+ "typescript.preferences.autoImportFileExcludePatterns": [
+ "@remix-run/server-runtime",
+ "express",
+ "@radix-ui/**",
+ "@react-email/**",
+ "node:stream/consumers",
+ "node:test",
+ "node:console"
+ ],
+ "workbench.editorAssociations": {
+ "*.db": "sqlite-viewer.view"
+ }
+}
diff --git a/examples/federation/epic-stack/CONTRIBUTING.md b/examples/federation/epic-stack/CONTRIBUTING.md
new file mode 100644
index 0000000..2a1e49f
--- /dev/null
+++ b/examples/federation/epic-stack/CONTRIBUTING.md
@@ -0,0 +1,87 @@
+# Contributing
+
+Thanks for your willingness to contribute! Please make sure to check with me
+before doing a bunch of work on something.
+
+## Project setup
+
+If you do need to set the project up locally yourself, feel free to follow these
+instructions:
+
+### System Requirements
+
+- [Node.js](https://nodejs.org/) >= 20.0.0
+- [npm](https://npmjs.com/) >= 8.18.0
+- [git](https://git-scm.com/) >= 2.38.0
+
+### Setup steps
+
+1. Fork repo
+2. clone the repo
+3. Copy `.env.example` into `.env`
+4. Run `npm install && npm run setup -s` to install dependencies and run
+ validation
+5. Create a branch for your PR with `git checkout -b pr/your-branch-name`
+
+> Tip: Keep your `main` branch pointing at the original repository and make pull
+> requests from branches on your fork. To do this, run:
+>
+> ```
+> git remote add upstream https://github.com/epicweb-dev/epic-stack.git
+> git fetch upstream
+> git branch --set-upstream-to=upstream/main main
+> ```
+>
+> This will add the original repository as a "remote" called "upstream," Then
+> fetch the git information from that remote, then set your local `main` branch
+> to use the upstream main branch whenever you run `git pull`. Then you can make
+> all of your pull request branches based on this `main` branch. Whenever you
+> want to update your version of `main`, do a regular `git pull`.
+
+If the setup script doesn't work, you can try to run the commands manually:
+
+```sh
+git clone
+cd ./epic-stack
+
+# copy the .env.example to .env
+# everything's mocked out during development so you shouldn't need to
+# change any of these values unless you want to hit real environments.
+cp .env.example .env
+
+# Install deps
+npm install
+
+# setup database
+prisma migrate reset --force
+
+# Install playwright browsers
+npm run test:e2e:install
+
+# run build, typecheck, linting
+npm run validate
+```
+
+If that all worked without trouble, you should be able to start development
+with:
+
+```sh
+npm run dev
+```
+
+And open up `http://localhost:3000` and rock!
+
+## Help Needed
+
+There's something to be said for custom code and the ability that grants with
+regard to tuning it to be exactly what you need. But there's also something to
+be said for offloading maintenance onto external dependencies. There are likely
+several bits of code in this codebase that could benefit from externalization.
+There could even be some things that could be improved by existing libraries.
+
+Feel free to take any code from within this project and turn it into an open
+source library (appropriate attribution is appreciated). Then come back and make
+a PR to use your new library.
+
+NOTE: Actual adoption of your library is not guaranteed. Offloading maintenance
+and adaptability is a delicate balance.
diff --git a/examples/federation/epic-stack/LICENSE.md b/examples/federation/epic-stack/LICENSE.md
new file mode 100644
index 0000000..7c76ba3
--- /dev/null
+++ b/examples/federation/epic-stack/LICENSE.md
@@ -0,0 +1,18 @@
+Copyright Β© 2023 Kent C. Dodds
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the βSoftwareβ), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED βAS ISβ, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/examples/federation/epic-stack/README.md b/examples/federation/epic-stack/README.md
new file mode 100644
index 0000000..cc8f52f
--- /dev/null
+++ b/examples/federation/epic-stack/README.md
@@ -0,0 +1,54 @@
+
+
+
+ Ditch analysis paralysis and start shipping Epic Web apps.
+
+
+ This is an opinionated project starter and reference that allows teams to
+ ship their ideas to production faster and on a more stable foundation based
+ on the experience of Kent C. Dodds and
+ contributors .
+
+
+
+```sh
+npx create-epic-app@latest
+```
+
+[](https://www.epicweb.dev/epic-stack)
+
+[The Epic Stack](https://www.epicweb.dev/epic-stack)
+
+
+
+## Watch Kent's Introduction to The Epic Stack
+
+[](https://www.epicweb.dev/talks/the-epic-stack)
+
+["The Epic Stack" by Kent C. Dodds](https://www.epicweb.dev/talks/the-epic-stack)
+
+## Docs
+
+[Read the docs](https://github.com/epicweb-dev/epic-stack/blob/main/docs)
+(please π).
+
+## Support
+
+- π Join the
+ [discussion on GitHub](https://github.com/epicweb-dev/epic-stack/discussions)
+ and the [KCD Community on Discord](https://kcd.im/discord).
+- π‘ Create an
+ [idea discussion](https://github.com/epicweb-dev/epic-stack/discussions/new?category=ideas)
+ for suggestions.
+- π Open a [GitHub issue](https://github.com/epicweb-dev/epic-stack/issues) to
+ report a bug.
+
+## Branding
+
+Want to talk about the Epic Stack in a blog post or talk? Great! Here are some
+assets you can use in your material:
+[EpicWeb.dev/brand](https://epicweb.dev/brand)
+
+## Thanks
+
+You rock πͺ¨
diff --git a/examples/federation/epic-stack/app/assets/favicons/apple-touch-icon.png b/examples/federation/epic-stack/app/assets/favicons/apple-touch-icon.png
new file mode 100644
index 0000000..8bf4632
Binary files /dev/null and b/examples/federation/epic-stack/app/assets/favicons/apple-touch-icon.png differ
diff --git a/examples/federation/epic-stack/app/assets/favicons/favicon.svg b/examples/federation/epic-stack/app/assets/favicons/favicon.svg
new file mode 100644
index 0000000..72be6f0
--- /dev/null
+++ b/examples/federation/epic-stack/app/assets/favicons/favicon.svg
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/examples/federation/epic-stack/app/components/error-boundary.tsx b/examples/federation/epic-stack/app/components/error-boundary.tsx
new file mode 100644
index 0000000..0aad131
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/error-boundary.tsx
@@ -0,0 +1,53 @@
+import { captureException } from '@sentry/react'
+import { useEffect, type ReactElement } from 'react'
+import {
+ type ErrorResponse,
+ isRouteErrorResponse,
+ useParams,
+ useRouteError,
+} from 'react-router'
+import { getErrorMessage } from '#app/utils/misc'
+
+type StatusHandler = (info: {
+ error: ErrorResponse
+ params: Record
+}) => ReactElement | null
+
+export function GeneralErrorBoundary({
+ defaultStatusHandler = ({ error }) => (
+
+ {error.status} {error.data}
+
+ ),
+ statusHandlers,
+ unexpectedErrorHandler = (error) => {getErrorMessage(error)}
,
+}: {
+ defaultStatusHandler?: StatusHandler
+ statusHandlers?: Record
+ unexpectedErrorHandler?: (error: unknown) => ReactElement | null
+}) {
+ const error = useRouteError()
+ const params = useParams()
+ const isResponse = isRouteErrorResponse(error)
+
+ if (typeof document !== 'undefined') {
+ console.error(error)
+ }
+
+ useEffect(() => {
+ if (isResponse) return
+
+ captureException(error)
+ }, [error, isResponse])
+
+ return (
+
+ {isResponse
+ ? (statusHandlers?.[error.status] ?? defaultStatusHandler)({
+ error,
+ params,
+ })
+ : unexpectedErrorHandler(error)}
+
+ )
+}
diff --git a/examples/federation/epic-stack/app/components/floating-toolbar.tsx b/examples/federation/epic-stack/app/components/floating-toolbar.tsx
new file mode 100644
index 0000000..41b5be0
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/floating-toolbar.tsx
@@ -0,0 +1,2 @@
+export const floatingToolbarClassName =
+ 'absolute bottom-3 left-3 right-3 flex items-center gap-2 rounded-lg bg-muted/80 p-4 pl-5 shadow-xl shadow-accent backdrop-blur-sm md:gap-4 md:pl-7 justify-end'
diff --git a/examples/federation/epic-stack/app/components/forms.tsx b/examples/federation/epic-stack/app/components/forms.tsx
new file mode 100644
index 0000000..9d3644a
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/forms.tsx
@@ -0,0 +1,202 @@
+import { useInputControl } from '@conform-to/react'
+import { REGEXP_ONLY_DIGITS_AND_CHARS, type OTPInputProps } from 'input-otp'
+import React, { useId } from 'react'
+import { Checkbox, type CheckboxProps } from './ui/checkbox.tsx'
+import {
+ InputOTP,
+ InputOTPGroup,
+ InputOTPSeparator,
+ InputOTPSlot,
+} from './ui/input-otp.tsx'
+import { Input } from './ui/input.tsx'
+import { Label } from './ui/label.tsx'
+import { Textarea } from './ui/textarea.tsx'
+
+export type ListOfErrors = Array | null | undefined
+
+export function ErrorList({
+ id,
+ errors,
+}: {
+ errors?: ListOfErrors
+ id?: string
+}) {
+ const errorsToRender = errors?.filter(Boolean)
+ if (!errorsToRender?.length) return null
+ return (
+
+ {errorsToRender.map((e) => (
+
+ {e}
+
+ ))}
+
+ )
+}
+
+export function Field({
+ labelProps,
+ inputProps,
+ errors,
+ className,
+}: {
+ labelProps: React.LabelHTMLAttributes
+ inputProps: React.InputHTMLAttributes
+ errors?: ListOfErrors
+ className?: string
+}) {
+ const fallbackId = useId()
+ const id = inputProps.id ?? fallbackId
+ const errorId = errors?.length ? `${id}-error` : undefined
+ return (
+
+
+
+
+ {errorId ? : null}
+
+
+ )
+}
+
+export function OTPField({
+ labelProps,
+ inputProps,
+ errors,
+ className,
+}: {
+ labelProps: React.LabelHTMLAttributes
+ inputProps: Partial
+ errors?: ListOfErrors
+ className?: string
+}) {
+ const fallbackId = useId()
+ const id = inputProps.id ?? fallbackId
+ const errorId = errors?.length ? `${id}-error` : undefined
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {errorId ? : null}
+
+
+ )
+}
+
+export function TextareaField({
+ labelProps,
+ textareaProps,
+ errors,
+ className,
+}: {
+ labelProps: React.LabelHTMLAttributes
+ textareaProps: React.TextareaHTMLAttributes
+ errors?: ListOfErrors
+ className?: string
+}) {
+ const fallbackId = useId()
+ const id = textareaProps.id ?? textareaProps.name ?? fallbackId
+ const errorId = errors?.length ? `${id}-error` : undefined
+ return (
+
+
+
+
+ {errorId ? : null}
+
+
+ )
+}
+
+export function CheckboxField({
+ labelProps,
+ buttonProps,
+ errors,
+ className,
+}: {
+ labelProps: React.ComponentProps<'label'>
+ buttonProps: CheckboxProps & {
+ name: string
+ form: string
+ value?: string
+ }
+ errors?: ListOfErrors
+ className?: string
+}) {
+ const { key, defaultChecked, ...checkboxProps } = buttonProps
+ const fallbackId = useId()
+ const checkedValue = buttonProps.value ?? 'on'
+ const input = useInputControl({
+ key,
+ name: buttonProps.name,
+ formId: buttonProps.form,
+ initialValue: defaultChecked ? checkedValue : undefined,
+ })
+ const id = buttonProps.id ?? fallbackId
+ const errorId = errors?.length ? `${id}-error` : undefined
+
+ return (
+
+
+ {
+ input.change(state.valueOf() ? checkedValue : '')
+ buttonProps.onCheckedChange?.(state)
+ }}
+ onFocus={(event) => {
+ input.focus()
+ buttonProps.onFocus?.(event)
+ }}
+ onBlur={(event) => {
+ input.blur()
+ buttonProps.onBlur?.(event)
+ }}
+ type="button"
+ />
+
+
+
+ {errorId ? : null}
+
+
+ )
+}
diff --git a/examples/federation/epic-stack/app/components/progress-bar.tsx b/examples/federation/epic-stack/app/components/progress-bar.tsx
new file mode 100644
index 0000000..d493f70
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/progress-bar.tsx
@@ -0,0 +1,63 @@
+import { useEffect, useRef, useState } from 'react'
+import { useNavigation } from 'react-router'
+import { useSpinDelay } from 'spin-delay'
+import { cn } from '#app/utils/misc.tsx'
+import { Icon } from './ui/icon.tsx'
+
+function EpicProgress() {
+ const transition = useNavigation()
+ const busy = transition.state !== 'idle'
+ const delayedPending = useSpinDelay(busy, {
+ delay: 600,
+ minDuration: 400,
+ })
+ const ref = useRef(null)
+ const [animationComplete, setAnimationComplete] = useState(true)
+
+ useEffect(() => {
+ if (!ref.current) return
+ if (delayedPending) setAnimationComplete(false)
+
+ const animationPromises = ref.current
+ .getAnimations()
+ .map(({ finished }) => finished)
+
+ void Promise.allSettled(animationPromises).then(() => {
+ if (!delayedPending) setAnimationComplete(true)
+ })
+ }, [delayedPending])
+
+ return (
+
+
+ {delayedPending && (
+
+
+
+ )}
+
+ )
+}
+
+export { EpicProgress }
diff --git a/examples/federation/epic-stack/app/components/search-bar.tsx b/examples/federation/epic-stack/app/components/search-bar.tsx
new file mode 100644
index 0000000..db7af73
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/search-bar.tsx
@@ -0,0 +1,63 @@
+import { useId } from 'react'
+import { Form, useSearchParams, useSubmit } from 'react-router'
+import { useDebounce, useIsPending } from '#app/utils/misc.tsx'
+import { Icon } from './ui/icon.tsx'
+import { Input } from './ui/input.tsx'
+import { Label } from './ui/label.tsx'
+import { StatusButton } from './ui/status-button.tsx'
+
+export function SearchBar({
+ status,
+ autoFocus = false,
+ autoSubmit = false,
+}: {
+ status: 'idle' | 'pending' | 'success' | 'error'
+ autoFocus?: boolean
+ autoSubmit?: boolean
+}) {
+ const id = useId()
+ const [searchParams] = useSearchParams()
+ const submit = useSubmit()
+ const isSubmitting = useIsPending({
+ formMethod: 'GET',
+ formAction: '/users',
+ })
+
+ const handleFormChange = useDebounce(async (form: HTMLFormElement) => {
+ await submit(form)
+ }, 400)
+
+ return (
+
+ )
+}
diff --git a/examples/federation/epic-stack/app/components/spacer.tsx b/examples/federation/epic-stack/app/components/spacer.tsx
new file mode 100644
index 0000000..8a8e537
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/spacer.tsx
@@ -0,0 +1,57 @@
+export function Spacer({
+ size,
+}: {
+ /**
+ * The size of the space
+ *
+ * 4xs: h-4 (16px)
+ *
+ * 3xs: h-8 (32px)
+ *
+ * 2xs: h-12 (48px)
+ *
+ * xs: h-16 (64px)
+ *
+ * sm: h-20 (80px)
+ *
+ * md: h-24 (96px)
+ *
+ * lg: h-28 (112px)
+ *
+ * xl: h-32 (128px)
+ *
+ * 2xl: h-36 (144px)
+ *
+ * 3xl: h-40 (160px)
+ *
+ * 4xl: h-44 (176px)
+ */
+ size:
+ | '4xs'
+ | '3xs'
+ | '2xs'
+ | 'xs'
+ | 'sm'
+ | 'md'
+ | 'lg'
+ | 'xl'
+ | '2xl'
+ | '3xl'
+ | '4xl'
+}) {
+ const options: Record = {
+ '4xs': 'h-4',
+ '3xs': 'h-8',
+ '2xs': 'h-12',
+ xs: 'h-16',
+ sm: 'h-20',
+ md: 'h-24',
+ lg: 'h-28',
+ xl: 'h-32',
+ '2xl': 'h-36',
+ '3xl': 'h-40',
+ '4xl': 'h-44',
+ }
+ const className = options[size]
+ return
+}
diff --git a/examples/federation/epic-stack/app/components/toaster.tsx b/examples/federation/epic-stack/app/components/toaster.tsx
new file mode 100644
index 0000000..954b2db
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/toaster.tsx
@@ -0,0 +1,16 @@
+import { useEffect } from 'react'
+import { toast as showToast } from 'sonner'
+import { type Toast } from '#app/utils/toast.server.ts'
+
+export function useToast(toast?: Toast | null) {
+ useEffect(() => {
+ if (toast) {
+ setTimeout(() => {
+ showToast[toast.type](toast.title, {
+ id: toast.id,
+ description: toast.description,
+ })
+ }, 0)
+ }
+ }, [toast])
+}
diff --git a/examples/federation/epic-stack/app/components/ui/README.md b/examples/federation/epic-stack/app/components/ui/README.md
new file mode 100644
index 0000000..433847d
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/ui/README.md
@@ -0,0 +1,7 @@
+# shadcn/ui
+
+Some components in this directory are downloaded via the
+[shadcn/ui](https://ui.shadcn.com) [CLI](https://ui.shadcn.com/docs/cli). Feel
+free to customize them to your needs. It's important to know that shadcn/ui is
+not a library of components you install, but instead it's a registry of prebuilt
+components which you can download and customize.
diff --git a/examples/federation/epic-stack/app/components/ui/button.tsx b/examples/federation/epic-stack/app/components/ui/button.tsx
new file mode 100644
index 0000000..36601c5
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/ui/button.tsx
@@ -0,0 +1,58 @@
+import { Slot } from '@radix-ui/react-slot'
+import { cva, type VariantProps } from 'class-variance-authority'
+import * as React from 'react'
+
+import { cn } from '#app/utils/misc.tsx'
+
+const buttonVariants = cva(
+ 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors outline-none focus-visible:ring-2 focus-within:ring-2 ring-ring ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground hover:bg-primary/80',
+ destructive:
+ 'bg-destructive text-destructive-foreground hover:bg-destructive/80',
+ outline:
+ 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
+ secondary:
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ size: {
+ default: 'h-10 px-4 py-2',
+ wide: 'px-24 py-5',
+ sm: 'h-9 rounded-md px-3',
+ lg: 'h-11 rounded-md px-8',
+ pill: 'px-12 py-3 leading-3',
+ icon: 'h-10 w-10',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'button'
+ return (
+
+ )
+ },
+)
+Button.displayName = 'Button'
+
+export { Button, buttonVariants }
diff --git a/examples/federation/epic-stack/app/components/ui/checkbox.tsx b/examples/federation/epic-stack/app/components/ui/checkbox.tsx
new file mode 100644
index 0000000..637a7fd
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/ui/checkbox.tsx
@@ -0,0 +1,41 @@
+import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
+import * as React from 'react'
+
+import { cn } from '#app/utils/misc.tsx'
+
+export type CheckboxProps = Omit<
+ React.ComponentPropsWithoutRef,
+ 'type'
+> & {
+ type?: string
+}
+
+const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+
+
+))
+Checkbox.displayName = CheckboxPrimitive.Root.displayName
+
+export { Checkbox }
diff --git a/examples/federation/epic-stack/app/components/ui/dropdown-menu.tsx b/examples/federation/epic-stack/app/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..3bb4fe3
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/ui/dropdown-menu.tsx
@@ -0,0 +1,206 @@
+import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
+import * as React from 'react'
+
+import { cn } from '#app/utils/misc.tsx'
+
+const DropdownMenu = DropdownMenuPrimitive.Root
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+ βΆοΈ
+
+))
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ βͺ
+
+
+ {children}
+
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+}
diff --git a/examples/federation/epic-stack/app/components/ui/icon.tsx b/examples/federation/epic-stack/app/components/ui/icon.tsx
new file mode 100644
index 0000000..10b8c1d
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/ui/icon.tsx
@@ -0,0 +1,77 @@
+import { type SVGProps } from 'react'
+import { cn } from '#app/utils/misc.tsx'
+import href from './icons/sprite.svg'
+import { type IconName } from '@/icon-name'
+
+export { href }
+export { IconName }
+
+const sizeClassName = {
+ font: 'w-[1em] h-[1em]',
+ xs: 'w-3 h-3',
+ sm: 'w-4 h-4',
+ md: 'w-5 h-5',
+ lg: 'w-6 h-6',
+ xl: 'w-7 h-7',
+} as const
+
+type Size = keyof typeof sizeClassName
+
+const childrenSizeClassName = {
+ font: 'gap-1.5',
+ xs: 'gap-1.5',
+ sm: 'gap-1.5',
+ md: 'gap-2',
+ lg: 'gap-2',
+ xl: 'gap-3',
+} satisfies Record
+
+/**
+ * Renders an SVG icon. The icon defaults to the size of the font. To make it
+ * align vertically with neighboring text, you can pass the text as a child of
+ * the icon and it will be automatically aligned.
+ * Alternatively, if you're not ok with the icon being to the left of the text,
+ * you need to wrap the icon and text in a common parent and set the parent to
+ * display "flex" (or "inline-flex") with "items-center" and a reasonable gap.
+ *
+ * Pass `title` prop to the `Icon` component to get `` element rendered
+ * in the SVG container, providing this way for accessibility.
+ */
+export function Icon({
+ name,
+ size = 'font',
+ className,
+ title,
+ children,
+ ...props
+}: SVGProps & {
+ name: IconName
+ size?: Size
+ title?: string
+}) {
+ if (children) {
+ return (
+
+
+ {children}
+
+ )
+ }
+ return (
+
+ {title ? {title} : null}
+
+
+ )
+}
diff --git a/examples/federation/epic-stack/app/components/ui/input-otp.tsx b/examples/federation/epic-stack/app/components/ui/input-otp.tsx
new file mode 100644
index 0000000..ff7a2d7
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/ui/input-otp.tsx
@@ -0,0 +1,70 @@
+import { OTPInput, OTPInputContext } from 'input-otp'
+import * as React from 'react'
+
+import { cn } from '#app/utils/misc.tsx'
+
+const InputOTP = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, containerClassName, ...props }, ref) => (
+
+))
+InputOTP.displayName = 'InputOTP'
+
+const InputOTPGroup = React.forwardRef<
+ React.ElementRef<'div'>,
+ React.ComponentPropsWithoutRef<'div'>
+>(({ className, ...props }, ref) => (
+
+))
+InputOTPGroup.displayName = 'InputOTPGroup'
+
+const InputOTPSlot = React.forwardRef<
+ React.ElementRef<'div'>,
+ React.ComponentPropsWithoutRef<'div'> & { index: number }
+>(({ index, className, ...props }, ref) => {
+ const inputOTPContext = React.useContext(OTPInputContext)
+ const slot = inputOTPContext.slots[index]
+ if (!slot) throw new Error('Invalid slot index')
+ const { char, hasFakeCaret, isActive } = slot
+
+ return (
+
+ {char}
+ {hasFakeCaret && (
+
+ )}
+
+ )
+})
+InputOTPSlot.displayName = 'InputOTPSlot'
+
+const InputOTPSeparator = React.forwardRef<
+ React.ElementRef<'div'>,
+ React.ComponentPropsWithoutRef<'div'>
+>(({ ...props }, ref) => (
+
+ -
+
+))
+InputOTPSeparator.displayName = 'InputOTPSeparator'
+
+export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
diff --git a/examples/federation/epic-stack/app/components/ui/input.tsx b/examples/federation/epic-stack/app/components/ui/input.tsx
new file mode 100644
index 0000000..f4b2db3
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react'
+
+import { cn } from '#app/utils/misc.tsx'
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ },
+)
+Input.displayName = 'Input'
+
+export { Input }
diff --git a/examples/federation/epic-stack/app/components/ui/label.tsx b/examples/federation/epic-stack/app/components/ui/label.tsx
new file mode 100644
index 0000000..ec453ee
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/ui/label.tsx
@@ -0,0 +1,24 @@
+import * as LabelPrimitive from '@radix-ui/react-label'
+import { cva, type VariantProps } from 'class-variance-authority'
+import * as React from 'react'
+
+import { cn } from '#app/utils/misc.tsx'
+
+const labelVariants = cva(
+ 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/examples/federation/epic-stack/app/components/ui/sonner.tsx b/examples/federation/epic-stack/app/components/ui/sonner.tsx
new file mode 100644
index 0000000..029a2a4
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/ui/sonner.tsx
@@ -0,0 +1,26 @@
+import { Toaster as Sonner } from 'sonner'
+
+type ToasterProps = React.ComponentProps
+
+const EpicToaster = ({ theme, ...props }: ToasterProps) => {
+ return (
+
+ )
+}
+
+export { EpicToaster }
diff --git a/examples/federation/epic-stack/app/components/ui/status-button.tsx b/examples/federation/epic-stack/app/components/ui/status-button.tsx
new file mode 100644
index 0000000..f39a7ff
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/ui/status-button.tsx
@@ -0,0 +1,78 @@
+import * as React from 'react'
+import { useSpinDelay } from 'spin-delay'
+import { cn } from '#app/utils/misc.tsx'
+import { Button, type ButtonProps } from './button.tsx'
+import { Icon } from './icon.tsx'
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from './tooltip.tsx'
+
+export const StatusButton = React.forwardRef<
+ HTMLButtonElement,
+ ButtonProps & {
+ status: 'pending' | 'success' | 'error' | 'idle'
+ message?: string | null
+ spinDelay?: Parameters[1]
+ }
+>(({ message, status, className, children, spinDelay, ...props }, ref) => {
+ const delayedPending = useSpinDelay(status === 'pending', {
+ delay: 400,
+ minDuration: 300,
+ ...spinDelay,
+ })
+ const companion = {
+ pending: delayedPending ? (
+
+
+
+ ) : null,
+ success: (
+
+
+
+ ),
+ error: (
+
+
+
+ ),
+ idle: null,
+ }[status]
+
+ return (
+
+ {children}
+ {message ? (
+
+
+ {companion}
+ {message}
+
+
+ ) : (
+ companion
+ )}
+
+ )
+})
+StatusButton.displayName = 'Button'
diff --git a/examples/federation/epic-stack/app/components/ui/textarea.tsx b/examples/federation/epic-stack/app/components/ui/textarea.tsx
new file mode 100644
index 0000000..2e8ea5e
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/ui/textarea.tsx
@@ -0,0 +1,24 @@
+import * as React from 'react'
+
+import { cn } from '#app/utils/misc.tsx'
+
+export interface TextareaProps
+ extends React.TextareaHTMLAttributes {}
+
+const Textarea = React.forwardRef(
+ ({ className, ...props }, ref) => {
+ return (
+
+ )
+ },
+)
+Textarea.displayName = 'Textarea'
+
+export { Textarea }
diff --git a/examples/federation/epic-stack/app/components/ui/tooltip.tsx b/examples/federation/epic-stack/app/components/ui/tooltip.tsx
new file mode 100644
index 0000000..5017f3e
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/ui/tooltip.tsx
@@ -0,0 +1,28 @@
+import * as TooltipPrimitive from '@radix-ui/react-tooltip'
+import * as React from 'react'
+
+import { cn } from '#app/utils/misc.tsx'
+
+const TooltipProvider = TooltipPrimitive.Provider
+
+const Tooltip = TooltipPrimitive.Root
+
+const TooltipTrigger = TooltipPrimitive.Trigger
+
+const TooltipContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+))
+TooltipContent.displayName = TooltipPrimitive.Content.displayName
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/examples/federation/epic-stack/app/components/user-dropdown.tsx b/examples/federation/epic-stack/app/components/user-dropdown.tsx
new file mode 100644
index 0000000..de7df44
--- /dev/null
+++ b/examples/federation/epic-stack/app/components/user-dropdown.tsx
@@ -0,0 +1,68 @@
+import { useRef } from 'react'
+import { Link, Form } from 'react-router'
+import { getUserImgSrc } from '#app/utils/misc.tsx'
+import { useUser } from '#app/utils/user.ts'
+import { Button } from './ui/button'
+import {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuPortal,
+ DropdownMenuContent,
+ DropdownMenuItem,
+} from './ui/dropdown-menu'
+import { Icon } from './ui/icon'
+
+export function UserDropdown() {
+ const user = useUser()
+ const formRef = useRef(null)
+ return (
+
+
+
+ e.preventDefault()}
+ className="flex items-center gap-2"
+ >
+
+
+ {user.name ?? user.username}
+
+
+
+
+
+
+
+
+
+ Profile
+
+
+
+
+
+
+ Notes
+
+
+
+
+
+
+
+ )
+}
diff --git a/examples/federation/epic-stack/app/entry.client.tsx b/examples/federation/epic-stack/app/entry.client.tsx
new file mode 100644
index 0000000..9b7749f
--- /dev/null
+++ b/examples/federation/epic-stack/app/entry.client.tsx
@@ -0,0 +1,11 @@
+import { startTransition } from 'react'
+import { hydrateRoot } from 'react-dom/client'
+import { HydratedRouter } from 'react-router/dom'
+
+if (ENV.MODE === 'production' && ENV.SENTRY_DSN) {
+ void import('./utils/monitoring.client.tsx').then(({ init }) => init())
+}
+
+startTransition(() => {
+ hydrateRoot(document, )
+})
diff --git a/examples/federation/epic-stack/app/entry.server.tsx b/examples/federation/epic-stack/app/entry.server.tsx
new file mode 100644
index 0000000..59adaf9
--- /dev/null
+++ b/examples/federation/epic-stack/app/entry.server.tsx
@@ -0,0 +1,116 @@
+import { PassThrough } from 'node:stream'
+import { createReadableStreamFromReadable } from '@react-router/node'
+
+import * as Sentry from '@sentry/node'
+import chalk from 'chalk'
+import { isbot } from 'isbot'
+import { renderToPipeableStream } from 'react-dom/server'
+import {
+ ServerRouter,
+ type LoaderFunctionArgs,
+ type ActionFunctionArgs,
+ type HandleDocumentRequestFunction,
+} from 'react-router'
+import { getEnv, init } from './utils/env.server.ts'
+import { getInstanceInfo } from './utils/litefs.server.ts'
+import { NonceProvider } from './utils/nonce-provider.ts'
+import { makeTimings } from './utils/timing.server.ts'
+
+export const streamTimeout = 5000
+
+init()
+global.ENV = getEnv()
+
+type DocRequestArgs = Parameters
+
+export default async function handleRequest(...args: DocRequestArgs) {
+ const [
+ request,
+ responseStatusCode,
+ responseHeaders,
+ reactRouterContext,
+ loadContext,
+ ] = args
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ responseHeaders.set('fly-primary-instance', primaryInstance)
+ responseHeaders.set('fly-instance', currentInstance)
+
+ if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
+ responseHeaders.append('Document-Policy', 'js-profiling')
+ }
+
+ const callbackName = isbot(request.headers.get('user-agent'))
+ ? 'onAllReady'
+ : 'onShellReady'
+
+ const nonce = loadContext.cspNonce?.toString() ?? ''
+ return new Promise(async (resolve, reject) => {
+ let didError = false
+ // NOTE: this timing will only include things that are rendered in the shell
+ // and will not include suspended components and deferred loaders
+ const timings = makeTimings('render', 'renderToPipeableStream')
+
+ const { pipe, abort } = renderToPipeableStream(
+
+
+ ,
+ {
+ [callbackName]: () => {
+ const body = new PassThrough()
+ responseHeaders.set('Content-Type', 'text/html')
+ responseHeaders.append('Server-Timing', timings.toString())
+ resolve(
+ new Response(createReadableStreamFromReadable(body), {
+ headers: responseHeaders,
+ status: didError ? 500 : responseStatusCode,
+ }),
+ )
+ pipe(body)
+ },
+ onShellError: (err: unknown) => {
+ reject(err)
+ },
+ onError: () => {
+ didError = true
+ },
+ nonce,
+ },
+ )
+
+ setTimeout(abort, streamTimeout + 5000)
+ })
+}
+
+export async function handleDataRequest(response: Response) {
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ response.headers.set('fly-primary-instance', primaryInstance)
+ response.headers.set('fly-instance', currentInstance)
+
+ return response
+}
+
+export function handleError(
+ error: unknown,
+ { request }: LoaderFunctionArgs | ActionFunctionArgs,
+): void {
+ // Skip capturing if the request is aborted as Remix docs suggest
+ // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
+ if (request.signal.aborted) {
+ return
+ }
+ if (error instanceof Error) {
+ console.error(chalk.red(error.stack))
+ void Sentry.captureException(error)
+ } else {
+ console.error(error)
+ Sentry.captureException(error)
+ }
+}
diff --git a/examples/federation/epic-stack/app/root.tsx b/examples/federation/epic-stack/app/root.tsx
new file mode 100644
index 0000000..da69cdd
--- /dev/null
+++ b/examples/federation/epic-stack/app/root.tsx
@@ -0,0 +1,260 @@
+import {
+ data,
+ Link,
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+ useLoaderData,
+ useMatches,
+} from 'react-router'
+import { HoneypotProvider } from 'remix-utils/honeypot/react'
+import { type Route } from './+types/root.ts'
+import appleTouchIconAssetUrl from './assets/favicons/apple-touch-icon.png'
+import faviconAssetUrl from './assets/favicons/favicon.svg'
+import { GeneralErrorBoundary } from './components/error-boundary.tsx'
+import { EpicProgress } from './components/progress-bar.tsx'
+import { SearchBar } from './components/search-bar.tsx'
+import { useToast } from './components/toaster.tsx'
+import { Button } from './components/ui/button.tsx'
+import { href as iconsHref } from './components/ui/icon.tsx'
+import { EpicToaster } from './components/ui/sonner.tsx'
+import { UserDropdown } from './components/user-dropdown.tsx'
+import {
+ ThemeSwitch,
+ useOptionalTheme,
+ useTheme,
+} from './routes/resources+/theme-switch.tsx'
+// import tailwindStyleSheetUrl from './styles/tailwind.css?url'
+import { getUserId, logout } from './utils/auth.server.ts'
+import { ClientHintCheck, getHints } from './utils/client-hints.tsx'
+import { prisma } from './utils/db.server.ts'
+import { getEnv } from './utils/env.server.ts'
+import { pipeHeaders } from './utils/headers.server.ts'
+import { honeypot } from './utils/honeypot.server.ts'
+import { combineHeaders, getDomainUrl } from './utils/misc.tsx'
+import { useNonce } from './utils/nonce-provider.ts'
+import { type Theme, getTheme } from './utils/theme.server.ts'
+import { makeTimings, time } from './utils/timing.server.ts'
+import { getToast } from './utils/toast.server.ts'
+import { useOptionalUser } from './utils/user.ts'
+import "./styles/tailwind.css"
+
+export const links: Route.LinksFunction = () => {
+ return [
+ // Preload svg sprite as a resource to avoid render blocking
+ { rel: 'preload', href: iconsHref, as: 'image' },
+ {
+ rel: 'icon',
+ href: '/favicon.ico',
+ sizes: '48x48',
+ },
+ { rel: 'icon', type: 'image/svg+xml', href: faviconAssetUrl },
+ { rel: 'apple-touch-icon', href: appleTouchIconAssetUrl },
+ {
+ rel: 'manifest',
+ href: '/site.webmanifest',
+ crossOrigin: 'use-credentials',
+ } as const, // necessary to make typescript happy
+ // { rel: 'stylesheet', href: tailwindStyleSheetUrl },
+ ].filter(Boolean)
+}
+
+export const meta: Route.MetaFunction = ({ data }) => {
+ return [
+ { title: data ? 'Epic Notes' : 'Error | Epic Notes' },
+ { name: 'description', content: `Your own captain's log` },
+ ]
+}
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const timings = makeTimings('root loader')
+ const userId = await time(() => getUserId(request), {
+ timings,
+ type: 'getUserId',
+ desc: 'getUserId in root',
+ })
+
+ const user = userId
+ ? await time(
+ () =>
+ prisma.user.findUnique({
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ image: { select: { id: true } },
+ roles: {
+ select: {
+ name: true,
+ permissions: {
+ select: { entity: true, action: true, access: true },
+ },
+ },
+ },
+ },
+ where: { id: userId },
+ }),
+ { timings, type: 'find user', desc: 'find user in root' },
+ )
+ : null
+ if (userId && !user) {
+ console.info('something weird happened')
+ // something weird happened... The user is authenticated but we can't find
+ // them in the database. Maybe they were deleted? Let's log them out.
+ await logout({ request, redirectTo: '/' })
+ }
+ const { toast, headers: toastHeaders } = await getToast(request)
+ const honeyProps = await honeypot.getInputProps()
+
+ return data(
+ {
+ user,
+ requestInfo: {
+ hints: getHints(request),
+ origin: getDomainUrl(request),
+ path: new URL(request.url).pathname,
+ userPrefs: {
+ theme: getTheme(request),
+ },
+ },
+ ENV: getEnv(),
+ toast,
+ honeyProps,
+ },
+ {
+ headers: combineHeaders(
+ { 'Server-Timing': timings.toString() },
+ toastHeaders,
+ ),
+ },
+ )
+}
+
+export const headers: Route.HeadersFunction = pipeHeaders
+
+function Document({
+ children,
+ nonce,
+ theme = 'light',
+ env = {},
+}: {
+ children: React.ReactNode
+ nonce: string
+ theme?: Theme
+ env?: Record
+}) {
+ const allowIndexing = ENV.ALLOW_INDEXING !== 'false'
+ return (
+
+
+
+
+
+
+ {allowIndexing ? null : (
+
+ )}
+
+
+
+ {children}
+
+
+
+
+
+ )
+}
+
+export function Layout({ children }: { children: React.ReactNode }) {
+ // if there was an error running the loader, data could be missing
+ const data = useLoaderData()
+ const nonce = useNonce()
+ const theme = useOptionalTheme()
+ return (
+
+ {children}
+
+ )
+}
+
+function App() {
+ const data = useLoaderData()
+ const user = useOptionalUser()
+ const theme = useTheme()
+ const matches = useMatches()
+ const isOnSearchPage = matches.find((m) => m.id === 'routes/users+/index')
+ const searchBar = isOnSearchPage ? null :
+ useToast(data.toast)
+
+ return (
+ <>
+
+
+
+ >
+ )
+}
+
+function Logo() {
+ return (
+
+
+ epic
+
+
+ notes
+
+
+ )
+}
+
+function AppWithProviders() {
+ const data = useLoaderData()
+ return (
+
+
+
+ )
+}
+
+export default AppWithProviders
+
+// this is a last resort error boundary. There's not much useful information we
+// can offer at this level.
+export const ErrorBoundary = GeneralErrorBoundary
diff --git a/examples/federation/epic-stack/app/routes.ts b/examples/federation/epic-stack/app/routes.ts
new file mode 100644
index 0000000..946ca4a
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes.ts
@@ -0,0 +1,21 @@
+import { type RouteConfig } from '@react-router/dev/routes'
+import { remixRoutesOptionAdapter } from '@react-router/remix-routes-option-adapter'
+import { flatRoutes } from 'remix-flat-routes'
+
+export default remixRoutesOptionAdapter((defineRoutes) => {
+ return flatRoutes('routes', defineRoutes, {
+ ignoredRouteFiles: [
+ '.*',
+ '**/*.css',
+ '**/*.test.{js,jsx,ts,tsx}',
+ '**/__*.*',
+ // This is for server-side utilities you want to colocate
+ // next to your routes without making an additional
+ // directory. If you need a route that includes "server" or
+ // "client" in the filename, use the escape brackets like:
+ // my-route.[server].tsx
+ '**/*.server.*',
+ '**/*.client.*',
+ ],
+ })
+}) satisfies RouteConfig
diff --git a/examples/federation/epic-stack/app/routes/$.tsx b/examples/federation/epic-stack/app/routes/$.tsx
new file mode 100644
index 0000000..cd695e3
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/$.tsx
@@ -0,0 +1,47 @@
+// This is called a "splat route" and as it's in the root `/app/routes/`
+// directory, it's a catchall. If no other routes match, this one will and we
+// can know that the user is hitting a URL that doesn't exist. By throwing a
+// 404 from the loader, we can force the error boundary to render which will
+// ensure the user gets the right status code and we can display a nicer error
+// message for them than the Remix and/or browser default.
+
+import { Link, useLocation } from 'react-router'
+import { GeneralErrorBoundary } from 'remote/components/error-boundary'
+import { Icon } from '#app/components/ui/icon.tsx'
+
+export function loader() {
+ throw new Response('Not found', { status: 404 })
+}
+
+export function action() {
+ throw new Response('Not found', { status: 404 })
+}
+
+export default function NotFound() {
+ // due to the loader, this component will never be rendered, but we'll return
+ // the error boundary just in case.
+ return
+}
+
+export function ErrorBoundary() {
+ const location = useLocation()
+ return (
+ (
+
+
+
We can't find this page:
+
+ {location.pathname}
+
+
+
+
Back to home
+
+
+ ),
+ }}
+ />
+ )
+}
diff --git a/examples/federation/epic-stack/app/routes/_auth+/auth.$provider.callback.test.ts b/examples/federation/epic-stack/app/routes/_auth+/auth.$provider.callback.test.ts
new file mode 100644
index 0000000..443b49b
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_auth+/auth.$provider.callback.test.ts
@@ -0,0 +1,259 @@
+import { invariant } from '@epic-web/invariant'
+import { faker } from '@faker-js/faker'
+import { http } from 'msw'
+import { afterEach, expect, test } from 'vitest'
+import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx'
+import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts'
+import { connectionSessionStorage } from '#app/utils/connections.server.ts'
+import { GITHUB_PROVIDER_NAME } from '#app/utils/connections.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { generateTOTP } from '#app/utils/totp.server.ts'
+import { createUser } from '#tests/db-utils.ts'
+import { insertGitHubUser, deleteGitHubUsers } from '#tests/mocks/github.ts'
+import { server } from '#tests/mocks/index.ts'
+import { consoleError } from '#tests/setup/setup-test-env.ts'
+import { BASE_URL, convertSetCookieToCookie } from '#tests/utils.ts'
+import { loader } from './auth.$provider.callback.ts'
+
+const ROUTE_PATH = '/auth/github/callback'
+const PARAMS = { provider: 'github' }
+
+afterEach(async () => {
+ await deleteGitHubUsers()
+})
+
+test('a new user goes to onboarding', async () => {
+ const request = await setupRequest()
+ const response = await loader({ request, params: PARAMS, context: {} }).catch(
+ (e) => e,
+ )
+ expect(response).toHaveRedirect('/onboarding/github')
+})
+
+test('when auth fails, send the user to login with a toast', async () => {
+ consoleError.mockImplementation(() => {})
+ server.use(
+ http.post('https://github.com/login/oauth/access_token', async () => {
+ return new Response('error', { status: 400 })
+ }),
+ )
+ const request = await setupRequest()
+ const response = await loader({ request, params: PARAMS, context: {} }).catch(
+ (e) => e,
+ )
+ invariant(response instanceof Response, 'response should be a Response')
+ expect(response).toHaveRedirect('/login')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Auth Failed',
+ type: 'error',
+ }),
+ )
+ expect(consoleError).toHaveBeenCalledTimes(1)
+})
+
+test('when a user is logged in, it creates the connection', async () => {
+ const githubUser = await insertGitHubUser()
+ const session = await setupUser()
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Connected',
+ type: 'success',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+ const connection = await prisma.connection.findFirst({
+ select: { id: true },
+ where: {
+ userId: session.userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ expect(
+ connection,
+ 'the connection was not created in the database',
+ ).toBeTruthy()
+})
+
+test(`when a user is logged in and has already connected, it doesn't do anything and just redirects the user back to the connections page`, async () => {
+ const session = await setupUser()
+ const githubUser = await insertGitHubUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ userId: session.userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Already Connected',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+})
+
+test('when a user exists with the same email, create connection and make session', async () => {
+ const githubUser = await insertGitHubUser()
+ const email = githubUser.primaryEmail.toLowerCase()
+ const { userId } = await setupUser({ ...createUser(), email })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+
+ expect(response).toHaveRedirect('/')
+
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ type: 'message',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+
+ const connection = await prisma.connection.findFirst({
+ select: { id: true },
+ where: {
+ userId: userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ expect(
+ connection,
+ 'the connection was not created in the database',
+ ).toBeTruthy()
+
+ await expect(response).toHaveSessionForUser(userId)
+})
+
+test('gives an error if the account is already connected to another user', async () => {
+ const githubUser = await insertGitHubUser()
+ await prisma.user.create({
+ data: {
+ ...createUser(),
+ connections: {
+ create: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ },
+ },
+ },
+ })
+ const session = await setupUser()
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Already Connected',
+ description: expect.stringContaining(
+ 'already connected to another account',
+ ),
+ }),
+ )
+})
+
+test('if a user is not logged in, but the connection exists, make a session', async () => {
+ const githubUser = await insertGitHubUser()
+ const { userId } = await setupUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ userId,
+ },
+ })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/')
+ await expect(response).toHaveSessionForUser(userId)
+})
+
+test('if a user is not logged in, but the connection exists and they have enabled 2FA, send them to verify their 2FA and do not make a session', async () => {
+ const githubUser = await insertGitHubUser()
+ const { userId } = await setupUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ userId,
+ },
+ })
+ const { otp: _otp, ...config } = await generateTOTP()
+ await prisma.verification.create({
+ data: {
+ type: twoFAVerificationType,
+ target: userId,
+ ...config,
+ },
+ })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ const searchParams = new URLSearchParams({
+ type: twoFAVerificationType,
+ target: userId,
+ redirectTo: '/',
+ })
+ expect(response).toHaveRedirect(`/verify?${searchParams}`)
+})
+
+async function setupRequest({
+ sessionId,
+ code = faker.string.uuid(),
+}: { sessionId?: string; code?: string } = {}) {
+ const url = new URL(ROUTE_PATH, BASE_URL)
+ const state = faker.string.uuid()
+ url.searchParams.set('state', state)
+ url.searchParams.set('code', code)
+ const connectionSession = await connectionSessionStorage.getSession()
+ connectionSession.set('oauth2:state', state)
+ const authSession = await authSessionStorage.getSession()
+ if (sessionId) authSession.set(sessionKey, sessionId)
+ const setSessionCookieHeader =
+ await authSessionStorage.commitSession(authSession)
+ const setConnectionSessionCookieHeader =
+ await connectionSessionStorage.commitSession(connectionSession)
+ const request = new Request(url.toString(), {
+ method: 'GET',
+ headers: {
+ cookie: [
+ convertSetCookieToCookie(setConnectionSessionCookieHeader),
+ convertSetCookieToCookie(setSessionCookieHeader),
+ ].join('; '),
+ },
+ })
+ return request
+}
+
+async function setupUser(userData = createUser()) {
+ const session = await prisma.session.create({
+ data: {
+ expirationDate: getSessionExpirationDate(),
+ user: {
+ create: {
+ ...userData,
+ },
+ },
+ },
+ select: {
+ id: true,
+ userId: true,
+ },
+ })
+
+ return session
+}
diff --git a/examples/federation/epic-stack/app/routes/_auth+/auth.$provider.callback.ts b/examples/federation/epic-stack/app/routes/_auth+/auth.$provider.callback.ts
new file mode 100644
index 0000000..bbbe564
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_auth+/auth.$provider.callback.ts
@@ -0,0 +1,197 @@
+import { redirect } from 'react-router'
+import {
+ authenticator,
+ getSessionExpirationDate,
+ getUserId,
+} from '#app/utils/auth.server.ts'
+import { ProviderNameSchema, providerLabels } from '#app/utils/connections.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { ensurePrimary } from '#app/utils/litefs.server.ts'
+import { combineHeaders } from '#app/utils/misc.tsx'
+import {
+ normalizeEmail,
+ normalizeUsername,
+} from '#app/utils/providers/provider.ts'
+import {
+ destroyRedirectToHeader,
+ getRedirectCookieValue,
+} from '#app/utils/redirect-cookie.server.ts'
+import {
+ createToastHeaders,
+ redirectWithToast,
+} from '#app/utils/toast.server.ts'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { type Route } from './+types/auth.$provider.callback.ts'
+import { handleNewSession } from './login.server.ts'
+import { onboardingEmailSessionKey } from './onboarding.tsx'
+import { prefilledProfileKey, providerIdKey } from './onboarding_.$provider.tsx'
+
+const destroyRedirectTo = { 'set-cookie': destroyRedirectToHeader }
+
+export async function loader({ request, params }: Route.LoaderArgs) {
+ // this loader performs mutations, so we need to make sure we're on the
+ // primary instance to avoid writing to a read-only replica
+ await ensurePrimary()
+
+ const providerName = ProviderNameSchema.parse(params.provider)
+ const redirectTo = getRedirectCookieValue(request)
+ const label = providerLabels[providerName]
+
+ const authResult = await authenticator
+ .authenticate(providerName, request, { throwOnError: true })
+ .then(
+ (data) =>
+ ({
+ success: true,
+ data,
+ }) as const,
+ (error) =>
+ ({
+ success: false,
+ error,
+ }) as const,
+ )
+
+ if (!authResult.success) {
+ console.error(authResult.error)
+ throw await redirectWithToast(
+ '/login',
+ {
+ title: 'Auth Failed',
+ description: `There was an error authenticating with ${label}.`,
+ type: 'error',
+ },
+ { headers: destroyRedirectTo },
+ )
+ }
+
+ const { data: profile } = authResult
+
+ const existingConnection = await prisma.connection.findUnique({
+ select: { userId: true },
+ where: {
+ providerName_providerId: { providerName, providerId: profile.id },
+ },
+ })
+
+ const userId = await getUserId(request)
+
+ if (existingConnection && userId) {
+ if (existingConnection.userId === userId) {
+ return redirectWithToast(
+ '/settings/profile/connections',
+ {
+ title: 'Already Connected',
+ description: `Your "${profile.username}" ${label} account is already connected.`,
+ },
+ { headers: destroyRedirectTo },
+ )
+ } else {
+ return redirectWithToast(
+ '/settings/profile/connections',
+ {
+ title: 'Already Connected',
+ description: `The "${profile.username}" ${label} account is already connected to another account.`,
+ },
+ { headers: destroyRedirectTo },
+ )
+ }
+ }
+
+ // If we're already logged in, then link the account
+ if (userId) {
+ await prisma.connection.create({
+ data: {
+ providerName,
+ providerId: profile.id,
+ userId,
+ },
+ })
+ return redirectWithToast(
+ '/settings/profile/connections',
+ {
+ title: 'Connected',
+ type: 'success',
+ description: `Your "${profile.username}" ${label} account has been connected.`,
+ },
+ { headers: destroyRedirectTo },
+ )
+ }
+
+ // Connection exists already? Make a new session
+ if (existingConnection) {
+ return makeSession({ request, userId: existingConnection.userId })
+ }
+
+ // if the email matches a user in the db, then link the account and
+ // make a new session
+ const user = await prisma.user.findUnique({
+ select: { id: true },
+ where: { email: profile.email.toLowerCase() },
+ })
+ if (user) {
+ await prisma.connection.create({
+ data: {
+ providerName,
+ providerId: profile.id,
+ userId: user.id,
+ },
+ })
+ return makeSession(
+ { request, userId: user.id },
+ {
+ headers: await createToastHeaders({
+ title: 'Connected',
+ description: `Your "${profile.username}" ${label} account has been connected.`,
+ }),
+ },
+ )
+ }
+
+ // this is a new user, so let's get them onboarded
+ const verifySession = await verifySessionStorage.getSession()
+ verifySession.set(onboardingEmailSessionKey, profile.email)
+ verifySession.set(prefilledProfileKey, {
+ ...profile,
+ email: normalizeEmail(profile.email),
+ username:
+ typeof profile.username === 'string'
+ ? normalizeUsername(profile.username)
+ : undefined,
+ })
+ verifySession.set(providerIdKey, profile.id)
+ const onboardingRedirect = [
+ `/onboarding/${providerName}`,
+ redirectTo ? new URLSearchParams({ redirectTo }) : null,
+ ]
+ .filter(Boolean)
+ .join('?')
+ return redirect(onboardingRedirect, {
+ headers: combineHeaders(
+ { 'set-cookie': await verifySessionStorage.commitSession(verifySession) },
+ destroyRedirectTo,
+ ),
+ })
+}
+
+async function makeSession(
+ {
+ request,
+ userId,
+ redirectTo,
+ }: { request: Request; userId: string; redirectTo?: string | null },
+ responseInit?: ResponseInit,
+) {
+ redirectTo ??= '/'
+ const session = await prisma.session.create({
+ select: { id: true, expirationDate: true, userId: true },
+ data: {
+ expirationDate: getSessionExpirationDate(),
+ userId,
+ },
+ })
+ return handleNewSession(
+ { request, session, redirectTo, remember: true },
+ { headers: combineHeaders(responseInit?.headers, destroyRedirectTo) },
+ )
+}
diff --git a/examples/federation/epic-stack/app/routes/_auth+/auth_.$provider.ts b/examples/federation/epic-stack/app/routes/_auth+/auth_.$provider.ts
new file mode 100644
index 0000000..b604005
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_auth+/auth_.$provider.ts
@@ -0,0 +1,34 @@
+import { redirect } from 'react-router'
+import { authenticator } from '#app/utils/auth.server.ts'
+import { handleMockAction } from '#app/utils/connections.server.ts'
+import { ProviderNameSchema } from '#app/utils/connections.tsx'
+import { getReferrerRoute } from '#app/utils/misc.tsx'
+import { getRedirectCookieHeader } from '#app/utils/redirect-cookie.server.ts'
+import { type Route } from './+types/auth_.$provider.ts'
+
+export async function loader() {
+ return redirect('/login')
+}
+
+export async function action({ request, params }: Route.ActionArgs) {
+ const providerName = ProviderNameSchema.parse(params.provider)
+
+ try {
+ await handleMockAction(providerName, request)
+ return await authenticator.authenticate(providerName, request)
+ } catch (error: unknown) {
+ if (error instanceof Response) {
+ const formData = await request.formData()
+ const rawRedirectTo = formData.get('redirectTo')
+ const redirectTo =
+ typeof rawRedirectTo === 'string'
+ ? rawRedirectTo
+ : getReferrerRoute(request)
+ const redirectToCookie = getRedirectCookieHeader(redirectTo)
+ if (redirectToCookie) {
+ error.headers.append('set-cookie', redirectToCookie)
+ }
+ }
+ throw error
+ }
+}
diff --git a/examples/federation/epic-stack/app/routes/_auth+/forgot-password.tsx b/examples/federation/epic-stack/app/routes/_auth+/forgot-password.tsx
new file mode 100644
index 0000000..b3c157a
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_auth+/forgot-password.tsx
@@ -0,0 +1,189 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import * as E from '@react-email/components'
+import { data, redirect, Link, useFetcher } from 'react-router'
+import { HoneypotInputs } from 'remix-utils/honeypot/react'
+import { z } from 'zod'
+import { GeneralErrorBoundary } from 'remote/components/error-boundary'
+import { ErrorList, Field } from 'remote/components/forms'
+import { StatusButton } from 'remote/components/ui/status-button'
+import { prisma } from '#app/utils/db.server.ts'
+import { sendEmail } from '#app/utils/email.server.ts'
+import { checkHoneypot } from '#app/utils/honeypot.server.ts'
+import { EmailSchema, UsernameSchema } from '#app/utils/user-validation.ts'
+import { type Route } from './+types/forgot-password.ts'
+import { prepareVerification } from './verify.server.ts'
+
+export const handle: SEOHandle = {
+ getSitemapEntries: () => null,
+}
+
+const ForgotPasswordSchema = z.object({
+ usernameOrEmail: z.union([EmailSchema, UsernameSchema]),
+})
+
+export async function action({ request }: Route.ActionArgs) {
+ const formData = await request.formData()
+ await checkHoneypot(formData)
+ const submission = await parseWithZod(formData, {
+ schema: ForgotPasswordSchema.superRefine(async (data, ctx) => {
+ const user = await prisma.user.findFirst({
+ where: {
+ OR: [
+ { email: data.usernameOrEmail },
+ { username: data.usernameOrEmail },
+ ],
+ },
+ select: { id: true },
+ })
+ if (!user) {
+ ctx.addIssue({
+ path: ['usernameOrEmail'],
+ code: z.ZodIssueCode.custom,
+ message: 'No user exists with this username or email',
+ })
+ return
+ }
+ }),
+ async: true,
+ })
+ if (submission.status !== 'success') {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+ const { usernameOrEmail } = submission.value
+
+ const user = await prisma.user.findFirstOrThrow({
+ where: { OR: [{ email: usernameOrEmail }, { username: usernameOrEmail }] },
+ select: { email: true, username: true },
+ })
+
+ const { verifyUrl, redirectTo, otp } = await prepareVerification({
+ period: 10 * 60,
+ request,
+ type: 'reset-password',
+ target: usernameOrEmail,
+ })
+
+ const response = await sendEmail({
+ to: user.email,
+ subject: `Epic Notes Password Reset`,
+ react: (
+
+ ),
+ })
+
+ if (response.status === 'success') {
+ return redirect(redirectTo.toString())
+ } else {
+ return data(
+ { result: submission.reply({ formErrors: [response.error.message] }) },
+ { status: 500 },
+ )
+ }
+}
+
+function ForgotPasswordEmail({
+ onboardingUrl,
+ otp,
+}: {
+ onboardingUrl: string
+ otp: string
+}) {
+ return (
+
+
+
+ Epic Notes Password Reset
+
+
+
+ Here's your verification code: {otp}
+
+
+
+ Or click the link:
+
+ {onboardingUrl}
+
+
+ )
+}
+
+export const meta: Route.MetaFunction = () => {
+ return [{ title: 'Password Recovery for Epic Notes' }]
+}
+
+export default function ForgotPasswordRoute() {
+ const forgotPassword = useFetcher()
+
+ const [form, fields] = useForm({
+ id: 'forgot-password-form',
+ constraint: getZodConstraint(ForgotPasswordSchema),
+ lastResult: forgotPassword.data?.result,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: ForgotPasswordSchema })
+ },
+ shouldRevalidate: 'onBlur',
+ })
+
+ return (
+
+
+
+
Forgot Password
+
+ No worries, we'll send you reset instructions.
+
+
+
+
+
+
+
+
+
+
+
+
+ Recover password
+
+
+
+
+ Back to Login
+
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return
+}
diff --git a/examples/federation/epic-stack/app/routes/_auth+/login.server.ts b/examples/federation/epic-stack/app/routes/_auth+/login.server.ts
new file mode 100644
index 0000000..9d5cf0b
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_auth+/login.server.ts
@@ -0,0 +1,158 @@
+import { invariant } from '@epic-web/invariant'
+import { redirect } from 'react-router'
+import { safeRedirect } from 'remix-utils/safe-redirect'
+import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx'
+import { getUserId, sessionKey } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { combineResponseInits } from '#app/utils/misc.tsx'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { redirectWithToast } from '#app/utils/toast.server.ts'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { getRedirectToUrl, type VerifyFunctionArgs } from './verify.server.ts'
+
+const verifiedTimeKey = 'verified-time'
+const unverifiedSessionIdKey = 'unverified-session-id'
+const rememberKey = 'remember'
+
+export async function handleNewSession(
+ {
+ request,
+ session,
+ redirectTo,
+ remember,
+ }: {
+ request: Request
+ session: { userId: string; id: string; expirationDate: Date }
+ redirectTo?: string
+ remember: boolean
+ },
+ responseInit?: ResponseInit,
+) {
+ const verification = await prisma.verification.findUnique({
+ select: { id: true },
+ where: {
+ target_type: { target: session.userId, type: twoFAVerificationType },
+ },
+ })
+ const userHasTwoFactor = Boolean(verification)
+
+ if (userHasTwoFactor) {
+ const verifySession = await verifySessionStorage.getSession()
+ verifySession.set(unverifiedSessionIdKey, session.id)
+ verifySession.set(rememberKey, remember)
+ const redirectUrl = getRedirectToUrl({
+ request,
+ type: twoFAVerificationType,
+ target: session.userId,
+ redirectTo,
+ })
+ return redirect(
+ `${redirectUrl.pathname}?${redirectUrl.searchParams}`,
+ combineResponseInits(
+ {
+ headers: {
+ 'set-cookie':
+ await verifySessionStorage.commitSession(verifySession),
+ },
+ },
+ responseInit,
+ ),
+ )
+ } else {
+ const authSession = await authSessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ authSession.set(sessionKey, session.id)
+
+ return redirect(
+ safeRedirect(redirectTo),
+ combineResponseInits(
+ {
+ headers: {
+ 'set-cookie': await authSessionStorage.commitSession(authSession, {
+ expires: remember ? session.expirationDate : undefined,
+ }),
+ },
+ },
+ responseInit,
+ ),
+ )
+ }
+}
+
+export async function handleVerification({
+ request,
+ submission,
+}: VerifyFunctionArgs) {
+ invariant(
+ submission.status === 'success',
+ 'Submission should be successful by now',
+ )
+ const authSession = await authSessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const verifySession = await verifySessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+
+ const remember = verifySession.get(rememberKey)
+ const { redirectTo } = submission.value
+ const headers = new Headers()
+ authSession.set(verifiedTimeKey, Date.now())
+
+ const unverifiedSessionId = verifySession.get(unverifiedSessionIdKey)
+ if (unverifiedSessionId) {
+ const session = await prisma.session.findUnique({
+ select: { expirationDate: true },
+ where: { id: unverifiedSessionId },
+ })
+ if (!session) {
+ throw await redirectWithToast('/login', {
+ type: 'error',
+ title: 'Invalid session',
+ description: 'Could not find session to verify. Please try again.',
+ })
+ }
+ authSession.set(sessionKey, unverifiedSessionId)
+
+ headers.append(
+ 'set-cookie',
+ await authSessionStorage.commitSession(authSession, {
+ expires: remember ? session.expirationDate : undefined,
+ }),
+ )
+ } else {
+ headers.append(
+ 'set-cookie',
+ await authSessionStorage.commitSession(authSession),
+ )
+ }
+
+ headers.append(
+ 'set-cookie',
+ await verifySessionStorage.destroySession(verifySession),
+ )
+
+ return redirect(safeRedirect(redirectTo), { headers })
+}
+
+export async function shouldRequestTwoFA(request: Request) {
+ const authSession = await authSessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const verifySession = await verifySessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ if (verifySession.has(unverifiedSessionIdKey)) return true
+ const userId = await getUserId(request)
+ if (!userId) return false
+ // if it's over two hours since they last verified, we should request 2FA again
+ const userHasTwoFA = await prisma.verification.findUnique({
+ select: { id: true },
+ where: { target_type: { target: userId, type: twoFAVerificationType } },
+ })
+ if (!userHasTwoFA) return false
+ const verifiedTime = authSession.get(verifiedTimeKey) ?? new Date(0)
+ const twoHours = 1000 * 60 * 2
+ return Date.now() - verifiedTime > twoHours
+}
diff --git a/examples/federation/epic-stack/app/routes/_auth+/login.tsx b/examples/federation/epic-stack/app/routes/_auth+/login.tsx
new file mode 100644
index 0000000..b7c9a8b
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_auth+/login.tsx
@@ -0,0 +1,204 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { data, Form, Link, useSearchParams } from 'react-router'
+import { HoneypotInputs } from 'remix-utils/honeypot/react'
+import { z } from 'zod'
+import { GeneralErrorBoundary } from 'remote/components/error-boundary'
+import { CheckboxField, ErrorList, Field } from 'remote/components/forms'
+import { Spacer } from 'remote/components/spacer'
+import { StatusButton } from 'remote/components/ui/status-button'
+import { login, requireAnonymous } from '#app/utils/auth.server.ts'
+import {
+ ProviderConnectionForm,
+ providerNames,
+} from '#app/utils/connections.tsx'
+import { checkHoneypot } from '#app/utils/honeypot.server.ts'
+import { useIsPending } from '#app/utils/misc.tsx'
+import { PasswordSchema, UsernameSchema } from '#app/utils/user-validation.ts'
+import { type Route } from './+types/login.ts'
+import { handleNewSession } from './login.server.ts'
+
+export const handle: SEOHandle = {
+ getSitemapEntries: () => null,
+}
+
+const LoginFormSchema = z.object({
+ username: UsernameSchema,
+ password: PasswordSchema,
+ redirectTo: z.string().optional(),
+ remember: z.boolean().optional(),
+})
+
+export async function loader({ request }: Route.LoaderArgs) {
+ await requireAnonymous(request)
+ return {}
+}
+
+export async function action({ request }: Route.ActionArgs) {
+ await requireAnonymous(request)
+ const formData = await request.formData()
+ await checkHoneypot(formData)
+ const submission = await parseWithZod(formData, {
+ schema: (intent) =>
+ LoginFormSchema.transform(async (data, ctx) => {
+ if (intent !== null) return { ...data, session: null }
+
+ const session = await login(data)
+ if (!session) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: 'Invalid username or password',
+ })
+ return z.NEVER
+ }
+
+ return { ...data, session }
+ }),
+ async: true,
+ })
+
+ if (submission.status !== 'success' || !submission.value.session) {
+ return data(
+ { result: submission.reply({ hideFields: ['password'] }) },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+
+ const { session, remember, redirectTo } = submission.value
+
+ return handleNewSession({
+ request,
+ session,
+ remember: remember ?? false,
+ redirectTo,
+ })
+}
+
+export default function LoginPage({ actionData }: Route.ComponentProps) {
+ const isPending = useIsPending()
+ const [searchParams] = useSearchParams()
+ const redirectTo = searchParams.get('redirectTo')
+
+ const [form, fields] = useForm({
+ id: 'login-form',
+ constraint: getZodConstraint(LoginFormSchema),
+ defaultValue: { redirectTo },
+ lastResult: actionData?.result,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: LoginFormSchema })
+ },
+ shouldRevalidate: 'onBlur',
+ })
+
+ return (
+
+
+
+
Welcome back!
+
+ Please enter your details.
+
+
+
+
+
+
+
+
+ {providerNames.map((providerName) => (
+
+
+
+ ))}
+
+
+ New here?
+
+ Create an account
+
+
+
+
+
+
+ )
+}
+
+export const meta: Route.MetaFunction = () => {
+ return [{ title: 'Login to Epic Notes' }]
+}
+
+export function ErrorBoundary() {
+ return
+}
diff --git a/examples/federation/epic-stack/app/routes/_auth+/logout.tsx b/examples/federation/epic-stack/app/routes/_auth+/logout.tsx
new file mode 100644
index 0000000..e99e670
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_auth+/logout.tsx
@@ -0,0 +1,11 @@
+import { redirect } from 'react-router'
+import { logout } from '#app/utils/auth.server.ts'
+import { type Route } from './+types/logout.ts'
+
+export async function loader() {
+ return redirect('/')
+}
+
+export async function action({ request }: Route.ActionArgs) {
+ return logout({ request })
+}
diff --git a/examples/federation/epic-stack/app/routes/_auth+/onboarding.server.ts b/examples/federation/epic-stack/app/routes/_auth+/onboarding.server.ts
new file mode 100644
index 0000000..502ef80
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_auth+/onboarding.server.ts
@@ -0,0 +1,19 @@
+import { invariant } from '@epic-web/invariant'
+import { redirect } from 'react-router'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { onboardingEmailSessionKey } from './onboarding.tsx'
+import { type VerifyFunctionArgs } from './verify.server.ts'
+
+export async function handleVerification({ submission }: VerifyFunctionArgs) {
+ invariant(
+ submission.status === 'success',
+ 'Submission should be successful by now',
+ )
+ const verifySession = await verifySessionStorage.getSession()
+ verifySession.set(onboardingEmailSessionKey, submission.value.target)
+ return redirect('/onboarding', {
+ headers: {
+ 'set-cookie': await verifySessionStorage.commitSession(verifySession),
+ },
+ })
+}
diff --git a/examples/federation/epic-stack/app/routes/_auth+/onboarding.tsx b/examples/federation/epic-stack/app/routes/_auth+/onboarding.tsx
new file mode 100644
index 0000000..ff08e28
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_auth+/onboarding.tsx
@@ -0,0 +1,232 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { data, redirect, Form, useSearchParams } from 'react-router'
+import { HoneypotInputs } from 'remix-utils/honeypot/react'
+import { safeRedirect } from 'remix-utils/safe-redirect'
+import { z } from 'zod'
+import { CheckboxField, ErrorList, Field } from 'remote/components/forms'
+import { Spacer } from 'remote/components/spacer'
+import { StatusButton } from 'remote/components/ui/status-button'
+import { requireAnonymous, sessionKey, signup } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { checkHoneypot } from '#app/utils/honeypot.server.ts'
+import { useIsPending } from '#app/utils/misc.tsx'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { redirectWithToast } from '#app/utils/toast.server.ts'
+import {
+ NameSchema,
+ PasswordAndConfirmPasswordSchema,
+ UsernameSchema,
+} from '#app/utils/user-validation.ts'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { type Route } from './+types/onboarding.ts'
+
+export const onboardingEmailSessionKey = 'onboardingEmail'
+
+const SignupFormSchema = z
+ .object({
+ username: UsernameSchema,
+ name: NameSchema,
+ agreeToTermsOfServiceAndPrivacyPolicy: z.boolean({
+ required_error:
+ 'You must agree to the terms of service and privacy policy',
+ }),
+ remember: z.boolean().optional(),
+ redirectTo: z.string().optional(),
+ })
+ .and(PasswordAndConfirmPasswordSchema)
+
+async function requireOnboardingEmail(request: Request) {
+ await requireAnonymous(request)
+ const verifySession = await verifySessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const email = verifySession.get(onboardingEmailSessionKey)
+ if (typeof email !== 'string' || !email) {
+ throw redirect('/signup')
+ }
+ return email
+}
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const email = await requireOnboardingEmail(request)
+ return { email }
+}
+
+export async function action({ request }: Route.ActionArgs) {
+ const email = await requireOnboardingEmail(request)
+ const formData = await request.formData()
+ await checkHoneypot(formData)
+ const submission = await parseWithZod(formData, {
+ schema: (intent) =>
+ SignupFormSchema.superRefine(async (data, ctx) => {
+ const existingUser = await prisma.user.findUnique({
+ where: { username: data.username },
+ select: { id: true },
+ })
+ if (existingUser) {
+ ctx.addIssue({
+ path: ['username'],
+ code: z.ZodIssueCode.custom,
+ message: 'A user already exists with this username',
+ })
+ return
+ }
+ }).transform(async (data) => {
+ if (intent !== null) return { ...data, session: null }
+
+ const session = await signup({ ...data, email })
+ return { ...data, session }
+ }),
+ async: true,
+ })
+
+ if (submission.status !== 'success' || !submission.value.session) {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+
+ const { session, remember, redirectTo } = submission.value
+
+ const authSession = await authSessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ authSession.set(sessionKey, session.id)
+ const verifySession = await verifySessionStorage.getSession()
+ const headers = new Headers()
+ headers.append(
+ 'set-cookie',
+ await authSessionStorage.commitSession(authSession, {
+ expires: remember ? session.expirationDate : undefined,
+ }),
+ )
+ headers.append(
+ 'set-cookie',
+ await verifySessionStorage.destroySession(verifySession),
+ )
+
+ return redirectWithToast(
+ safeRedirect(redirectTo),
+ { title: 'Welcome', description: 'Thanks for signing up!' },
+ { headers },
+ )
+}
+
+export const meta: Route.MetaFunction = () => {
+ return [{ title: 'Setup Epic Notes Account' }]
+}
+
+export default function OnboardingRoute({
+ loaderData,
+ actionData,
+}: Route.ComponentProps) {
+ const isPending = useIsPending()
+ const [searchParams] = useSearchParams()
+ const redirectTo = searchParams.get('redirectTo')
+
+ const [form, fields] = useForm({
+ id: 'onboarding-form',
+ constraint: getZodConstraint(SignupFormSchema),
+ defaultValue: { redirectTo },
+ lastResult: actionData?.result,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: SignupFormSchema })
+ },
+ shouldRevalidate: 'onBlur',
+ })
+
+ return (
+
+
+
+
Welcome aboard {loaderData.email}!
+
+ Please enter your details.
+
+
+
+
+
+
+ )
+}
diff --git a/examples/federation/epic-stack/app/routes/_auth+/onboarding_.$provider.server.ts b/examples/federation/epic-stack/app/routes/_auth+/onboarding_.$provider.server.ts
new file mode 100644
index 0000000..502ef80
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_auth+/onboarding_.$provider.server.ts
@@ -0,0 +1,19 @@
+import { invariant } from '@epic-web/invariant'
+import { redirect } from 'react-router'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { onboardingEmailSessionKey } from './onboarding.tsx'
+import { type VerifyFunctionArgs } from './verify.server.ts'
+
+export async function handleVerification({ submission }: VerifyFunctionArgs) {
+ invariant(
+ submission.status === 'success',
+ 'Submission should be successful by now',
+ )
+ const verifySession = await verifySessionStorage.getSession()
+ verifySession.set(onboardingEmailSessionKey, submission.value.target)
+ return redirect('/onboarding', {
+ headers: {
+ 'set-cookie': await verifySessionStorage.commitSession(verifySession),
+ },
+ })
+}
diff --git a/examples/federation/epic-stack/app/routes/_auth+/onboarding_.$provider.tsx b/examples/federation/epic-stack/app/routes/_auth+/onboarding_.$provider.tsx
new file mode 100644
index 0000000..97cda0f
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_auth+/onboarding_.$provider.tsx
@@ -0,0 +1,280 @@
+import {
+ getFormProps,
+ getInputProps,
+ useForm,
+ type SubmissionResult,
+} from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import {
+ redirect,
+ data,
+ type Params,
+ Form,
+ useSearchParams,
+} from 'react-router'
+import { safeRedirect } from 'remix-utils/safe-redirect'
+import { z } from 'zod'
+import { CheckboxField, ErrorList, Field } from 'remote/components/forms'
+import { Spacer } from 'remote/components/spacer'
+import { StatusButton } from 'remote/components/ui/status-button'
+import {
+ authenticator,
+ sessionKey,
+ signupWithConnection,
+ requireAnonymous,
+} from '#app/utils/auth.server.ts'
+import { connectionSessionStorage } from '#app/utils/connections.server'
+import { ProviderNameSchema } from '#app/utils/connections.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { useIsPending } from '#app/utils/misc.tsx'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { redirectWithToast } from '#app/utils/toast.server.ts'
+import { NameSchema, UsernameSchema } from '#app/utils/user-validation.ts'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { type Route } from './+types/onboarding_.$provider.ts'
+import { onboardingEmailSessionKey } from './onboarding'
+
+export const providerIdKey = 'providerId'
+export const prefilledProfileKey = 'prefilledProfile'
+
+const SignupFormSchema = z.object({
+ imageUrl: z.string().optional(),
+ username: UsernameSchema,
+ name: NameSchema,
+ agreeToTermsOfServiceAndPrivacyPolicy: z.boolean({
+ required_error: 'You must agree to the terms of service and privacy policy',
+ }),
+ remember: z.boolean().optional(),
+ redirectTo: z.string().optional(),
+})
+
+async function requireData({
+ request,
+ params,
+}: {
+ request: Request
+ params: Params
+}) {
+ await requireAnonymous(request)
+ const verifySession = await verifySessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const email = verifySession.get(onboardingEmailSessionKey)
+ const providerId = verifySession.get(providerIdKey)
+ const result = z
+ .object({
+ email: z.string(),
+ providerName: ProviderNameSchema,
+ providerId: z.string(),
+ })
+ .safeParse({ email, providerName: params.provider, providerId })
+ if (result.success) {
+ return result.data
+ } else {
+ console.error(result.error)
+ throw redirect('/signup')
+ }
+}
+
+export async function loader({ request, params }: Route.LoaderArgs) {
+ const { email } = await requireData({ request, params })
+ const connectionSession = await connectionSessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const verifySession = await verifySessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const prefilledProfile = verifySession.get(prefilledProfileKey)
+
+ const formError = connectionSession.get(authenticator.sessionErrorKey)
+ const hasError = typeof formError === 'string'
+
+ return {
+ email,
+ status: 'idle',
+ submission: {
+ status: hasError ? 'error' : undefined,
+ initialValue: prefilledProfile ?? {},
+ error: { '': hasError ? [formError] : [] },
+ } as SubmissionResult,
+ }
+}
+
+export async function action({ request, params }: Route.ActionArgs) {
+ const { email, providerId, providerName } = await requireData({
+ request,
+ params,
+ })
+ const formData = await request.formData()
+ const verifySession = await verifySessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+
+ const submission = await parseWithZod(formData, {
+ schema: SignupFormSchema.superRefine(async (data, ctx) => {
+ const existingUser = await prisma.user.findUnique({
+ where: { username: data.username },
+ select: { id: true },
+ })
+ if (existingUser) {
+ ctx.addIssue({
+ path: ['username'],
+ code: z.ZodIssueCode.custom,
+ message: 'A user already exists with this username',
+ })
+ return
+ }
+ }).transform(async (data) => {
+ const session = await signupWithConnection({
+ ...data,
+ email,
+ providerId,
+ providerName,
+ })
+ return { ...data, session }
+ }),
+ async: true,
+ })
+
+ if (submission.status !== 'success') {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+
+ const { session, remember, redirectTo } = submission.value
+
+ const authSession = await authSessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ authSession.set(sessionKey, session.id)
+ const headers = new Headers()
+ headers.append(
+ 'set-cookie',
+ await authSessionStorage.commitSession(authSession, {
+ expires: remember ? session.expirationDate : undefined,
+ }),
+ )
+ headers.append(
+ 'set-cookie',
+ await verifySessionStorage.destroySession(verifySession),
+ )
+
+ return redirectWithToast(
+ safeRedirect(redirectTo),
+ { title: 'Welcome', description: 'Thanks for signing up!' },
+ { headers },
+ )
+}
+
+export const meta: Route.MetaFunction = () => {
+ return [{ title: 'Setup Epic Notes Account' }]
+}
+
+export default function OnboardingProviderRoute({
+ loaderData,
+ actionData,
+}: Route.ComponentProps) {
+ const isPending = useIsPending()
+ const [searchParams] = useSearchParams()
+ const redirectTo = searchParams.get('redirectTo')
+
+ const [form, fields] = useForm({
+ id: 'onboarding-provider-form',
+ constraint: getZodConstraint(SignupFormSchema),
+ lastResult: actionData?.result ?? loaderData.submission,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: SignupFormSchema })
+ },
+ shouldRevalidate: 'onBlur',
+ })
+
+ return (
+
+
+
+
Welcome aboard {loaderData.email}!
+
+ Please enter your details.
+
+
+
+
+
+
+ )
+}
diff --git a/examples/federation/epic-stack/app/routes/_auth+/reset-password.server.ts b/examples/federation/epic-stack/app/routes/_auth+/reset-password.server.ts
new file mode 100644
index 0000000..f5b2d50
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_auth+/reset-password.server.ts
@@ -0,0 +1,34 @@
+import { invariant } from '@epic-web/invariant'
+import { data, redirect } from 'react-router'
+import { prisma } from '#app/utils/db.server.ts'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { resetPasswordUsernameSessionKey } from './reset-password.tsx'
+import { type VerifyFunctionArgs } from './verify.server.ts'
+
+export async function handleVerification({ submission }: VerifyFunctionArgs) {
+ invariant(
+ submission.status === 'success',
+ 'Submission should be successful by now',
+ )
+ const target = submission.value.target
+ const user = await prisma.user.findFirst({
+ where: { OR: [{ email: target }, { username: target }] },
+ select: { email: true, username: true },
+ })
+ // we don't want to say the user is not found if the email is not found
+ // because that would allow an attacker to check if an email is registered
+ if (!user) {
+ return data(
+ { result: submission.reply({ fieldErrors: { code: ['Invalid code'] } }) },
+ { status: 400 },
+ )
+ }
+
+ const verifySession = await verifySessionStorage.getSession()
+ verifySession.set(resetPasswordUsernameSessionKey, user.username)
+ return redirect('/reset-password', {
+ headers: {
+ 'set-cookie': await verifySessionStorage.commitSession(verifySession),
+ },
+ })
+}
diff --git a/examples/federation/epic-stack/app/routes/_auth+/reset-password.tsx b/examples/federation/epic-stack/app/routes/_auth+/reset-password.tsx
new file mode 100644
index 0000000..a938dcb
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_auth+/reset-password.tsx
@@ -0,0 +1,137 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { data, redirect, Form } from 'react-router'
+import { GeneralErrorBoundary } from 'remote/components/error-boundary'
+import { ErrorList, Field } from 'remote/components/forms'
+import { StatusButton } from 'remote/components/ui/status-button'
+import { requireAnonymous, resetUserPassword } from '#app/utils/auth.server.ts'
+import { useIsPending } from '#app/utils/misc.tsx'
+import { PasswordAndConfirmPasswordSchema } from '#app/utils/user-validation.ts'
+import { verifySessionStorage } from '#app/utils/verification.server.ts'
+import { type Route } from './+types/reset-password.ts'
+
+export const handle: SEOHandle = {
+ getSitemapEntries: () => null,
+}
+
+export const resetPasswordUsernameSessionKey = 'resetPasswordUsername'
+
+const ResetPasswordSchema = PasswordAndConfirmPasswordSchema
+
+async function requireResetPasswordUsername(request: Request) {
+ await requireAnonymous(request)
+ const verifySession = await verifySessionStorage.getSession(
+ request.headers.get('cookie'),
+ )
+ const resetPasswordUsername = verifySession.get(
+ resetPasswordUsernameSessionKey,
+ )
+ if (typeof resetPasswordUsername !== 'string' || !resetPasswordUsername) {
+ throw redirect('/login')
+ }
+ return resetPasswordUsername
+}
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const resetPasswordUsername = await requireResetPasswordUsername(request)
+ return { resetPasswordUsername }
+}
+
+export async function action({ request }: Route.ActionArgs) {
+ const resetPasswordUsername = await requireResetPasswordUsername(request)
+ const formData = await request.formData()
+ const submission = parseWithZod(formData, {
+ schema: ResetPasswordSchema,
+ })
+ if (submission.status !== 'success') {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+ const { password } = submission.value
+
+ await resetUserPassword({ username: resetPasswordUsername, password })
+ const verifySession = await verifySessionStorage.getSession()
+ return redirect('/login', {
+ headers: {
+ 'set-cookie': await verifySessionStorage.destroySession(verifySession),
+ },
+ })
+}
+
+export const meta: Route.MetaFunction = () => {
+ return [{ title: 'Reset Password | Epic Notes' }]
+}
+
+export default function ResetPasswordPage({
+ loaderData,
+ actionData,
+}: Route.ComponentProps) {
+ const isPending = useIsPending()
+
+ const [form, fields] = useForm({
+ id: 'reset-password',
+ constraint: getZodConstraint(ResetPasswordSchema),
+ lastResult: actionData?.result,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: ResetPasswordSchema })
+ },
+ shouldRevalidate: 'onBlur',
+ })
+
+ return (
+
+
+
Password Reset
+
+ Hi, {loaderData.resetPasswordUsername}. No worries. It happens all the
+ time.
+
+
+
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return
+}
diff --git a/examples/federation/epic-stack/app/routes/_auth+/signup.tsx b/examples/federation/epic-stack/app/routes/_auth+/signup.tsx
new file mode 100644
index 0000000..c2ea07b
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_auth+/signup.tsx
@@ -0,0 +1,185 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import * as E from '@react-email/components'
+import { data, redirect, Form, useSearchParams } from 'react-router'
+import { HoneypotInputs } from 'remix-utils/honeypot/react'
+import { z } from 'zod'
+import { GeneralErrorBoundary } from 'remote/components/error-boundary'
+import { ErrorList, Field } from 'remote/components/forms'
+import { StatusButton } from 'remote/components/ui/status-button'
+import {
+ ProviderConnectionForm,
+ providerNames,
+} from '#app/utils/connections.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { sendEmail } from '#app/utils/email.server.ts'
+import { checkHoneypot } from '#app/utils/honeypot.server.ts'
+import { useIsPending } from '#app/utils/misc.tsx'
+import { EmailSchema } from '#app/utils/user-validation.ts'
+import { type Route } from './+types/signup.ts'
+import { prepareVerification } from './verify.server.ts'
+
+export const handle: SEOHandle = {
+ getSitemapEntries: () => null,
+}
+
+const SignupSchema = z.object({
+ email: EmailSchema,
+})
+
+export async function action({ request }: Route.ActionArgs) {
+ const formData = await request.formData()
+
+ await checkHoneypot(formData)
+
+ const submission = await parseWithZod(formData, {
+ schema: SignupSchema.superRefine(async (data, ctx) => {
+ const existingUser = await prisma.user.findUnique({
+ where: { email: data.email },
+ select: { id: true },
+ })
+ if (existingUser) {
+ ctx.addIssue({
+ path: ['email'],
+ code: z.ZodIssueCode.custom,
+ message: 'A user already exists with this email',
+ })
+ return
+ }
+ }),
+ async: true,
+ })
+ if (submission.status !== 'success') {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+ const { email } = submission.value
+ const { verifyUrl, redirectTo, otp } = await prepareVerification({
+ period: 10 * 60,
+ request,
+ type: 'onboarding',
+ target: email,
+ })
+
+ const response = await sendEmail({
+ to: email,
+ subject: `Welcome to Epic Notes!`,
+ react: ,
+ })
+
+ if (response.status === 'success') {
+ return redirect(redirectTo.toString())
+ } else {
+ return data(
+ {
+ result: submission.reply({ formErrors: [response.error.message] }),
+ },
+ {
+ status: 500,
+ },
+ )
+ }
+}
+
+export function SignupEmail({
+ onboardingUrl,
+ otp,
+}: {
+ onboardingUrl: string
+ otp: string
+}) {
+ return (
+
+
+
+ Welcome to Epic Notes!
+
+
+
+ Here's your verification code: {otp}
+
+
+
+ Or click the link to get started:
+
+ {onboardingUrl}
+
+
+ )
+}
+
+export const meta: Route.MetaFunction = () => {
+ return [{ title: 'Sign Up | Epic Notes' }]
+}
+
+export default function SignupRoute({ actionData }: Route.ComponentProps) {
+ const isPending = useIsPending()
+ const [searchParams] = useSearchParams()
+ const redirectTo = searchParams.get('redirectTo')
+
+ const [form, fields] = useForm({
+ id: 'signup-form',
+ constraint: getZodConstraint(SignupSchema),
+ lastResult: actionData?.result,
+ onValidate({ formData }) {
+ const result = parseWithZod(formData, { schema: SignupSchema })
+ return result
+ },
+ shouldRevalidate: 'onBlur',
+ })
+
+ return (
+
+
+
Let's start your journey!
+
+ Please enter your email.
+
+
+
+
+
+ {providerNames.map((providerName) => (
+
+
+
+ ))}
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return
+}
diff --git a/examples/federation/epic-stack/app/routes/_auth+/verify.server.ts b/examples/federation/epic-stack/app/routes/_auth+/verify.server.ts
new file mode 100644
index 0000000..e3b8e0e
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_auth+/verify.server.ts
@@ -0,0 +1,200 @@
+import { type Submission } from '@conform-to/react'
+import { parseWithZod } from '@conform-to/zod'
+import { data } from 'react-router'
+import { z } from 'zod'
+import { handleVerification as handleChangeEmailVerification } from '#app/routes/settings+/profile.change-email.server.tsx'
+import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx'
+import { requireUserId } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { getDomainUrl } from '#app/utils/misc.tsx'
+import { redirectWithToast } from '#app/utils/toast.server.ts'
+import { generateTOTP, verifyTOTP } from '#app/utils/totp.server.ts'
+import { type twoFAVerifyVerificationType } from '../settings+/profile.two-factor.verify.tsx'
+import {
+ handleVerification as handleLoginTwoFactorVerification,
+ shouldRequestTwoFA,
+} from './login.server.ts'
+import { handleVerification as handleOnboardingVerification } from './onboarding.server.ts'
+import { handleVerification as handleResetPasswordVerification } from './reset-password.server.ts'
+import {
+ VerifySchema,
+ codeQueryParam,
+ redirectToQueryParam,
+ targetQueryParam,
+ typeQueryParam,
+ type VerificationTypes,
+} from './verify.tsx'
+
+export type VerifyFunctionArgs = {
+ request: Request
+ submission: Submission<
+ z.input,
+ string[],
+ z.output
+ >
+ body: FormData | URLSearchParams
+}
+
+export function getRedirectToUrl({
+ request,
+ type,
+ target,
+ redirectTo,
+}: {
+ request: Request
+ type: VerificationTypes
+ target: string
+ redirectTo?: string
+}) {
+ const redirectToUrl = new URL(`${getDomainUrl(request)}/verify`)
+ redirectToUrl.searchParams.set(typeQueryParam, type)
+ redirectToUrl.searchParams.set(targetQueryParam, target)
+ if (redirectTo) {
+ redirectToUrl.searchParams.set(redirectToQueryParam, redirectTo)
+ }
+ return redirectToUrl
+}
+
+export async function requireRecentVerification(request: Request) {
+ const userId = await requireUserId(request)
+ const shouldReverify = await shouldRequestTwoFA(request)
+ if (shouldReverify) {
+ const reqUrl = new URL(request.url)
+ const redirectUrl = getRedirectToUrl({
+ request,
+ target: userId,
+ type: twoFAVerificationType,
+ redirectTo: reqUrl.pathname + reqUrl.search,
+ })
+ throw await redirectWithToast(redirectUrl.toString(), {
+ title: 'Please Reverify',
+ description: 'Please reverify your account before proceeding',
+ })
+ }
+}
+
+export async function prepareVerification({
+ period,
+ request,
+ type,
+ target,
+}: {
+ period: number
+ request: Request
+ type: VerificationTypes
+ target: string
+}) {
+ const verifyUrl = getRedirectToUrl({ request, type, target })
+ const redirectTo = new URL(verifyUrl.toString())
+
+ const { otp, ...verificationConfig } = await generateTOTP({
+ algorithm: 'SHA-256',
+ // Leaving off 0, O, and I on purpose to avoid confusing users.
+ charSet: 'ABCDEFGHJKLMNPQRSTUVWXYZ123456789',
+ period,
+ })
+ const verificationData = {
+ type,
+ target,
+ ...verificationConfig,
+ expiresAt: new Date(Date.now() + verificationConfig.period * 1000),
+ }
+ await prisma.verification.upsert({
+ where: { target_type: { target, type } },
+ create: verificationData,
+ update: verificationData,
+ })
+
+ // add the otp to the url we'll email the user.
+ verifyUrl.searchParams.set(codeQueryParam, otp)
+
+ return { otp, redirectTo, verifyUrl }
+}
+
+export async function isCodeValid({
+ code,
+ type,
+ target,
+}: {
+ code: string
+ type: VerificationTypes | typeof twoFAVerifyVerificationType
+ target: string
+}) {
+ const verification = await prisma.verification.findUnique({
+ where: {
+ target_type: { target, type },
+ OR: [{ expiresAt: { gt: new Date() } }, { expiresAt: null }],
+ },
+ select: { algorithm: true, secret: true, period: true, charSet: true },
+ })
+ if (!verification) return false
+ const result = await verifyTOTP({
+ otp: code,
+ ...verification,
+ })
+ if (!result) return false
+
+ return true
+}
+
+export async function validateRequest(
+ request: Request,
+ body: URLSearchParams | FormData,
+) {
+ const submission = await parseWithZod(body, {
+ schema: VerifySchema.superRefine(async (data, ctx) => {
+ const codeIsValid = await isCodeValid({
+ code: data[codeQueryParam],
+ type: data[typeQueryParam],
+ target: data[targetQueryParam],
+ })
+ if (!codeIsValid) {
+ ctx.addIssue({
+ path: ['code'],
+ code: z.ZodIssueCode.custom,
+ message: `Invalid code`,
+ })
+ return
+ }
+ }),
+ async: true,
+ })
+
+ if (submission.status !== 'success') {
+ return data(
+ { result: submission.reply() },
+ { status: submission.status === 'error' ? 400 : 200 },
+ )
+ }
+
+ const { value: submissionValue } = submission
+
+ async function deleteVerification() {
+ await prisma.verification.delete({
+ where: {
+ target_type: {
+ type: submissionValue[typeQueryParam],
+ target: submissionValue[targetQueryParam],
+ },
+ },
+ })
+ }
+
+ switch (submissionValue[typeQueryParam]) {
+ case 'reset-password': {
+ await deleteVerification()
+ return handleResetPasswordVerification({ request, body, submission })
+ }
+ case 'onboarding': {
+ await deleteVerification()
+ return handleOnboardingVerification({ request, body, submission })
+ }
+ case 'change-email': {
+ await deleteVerification()
+ return handleChangeEmailVerification({ request, body, submission })
+ }
+ case '2fa': {
+ return handleLoginTwoFactorVerification({ request, body, submission })
+ }
+ }
+}
diff --git a/examples/federation/epic-stack/app/routes/_auth+/verify.tsx b/examples/federation/epic-stack/app/routes/_auth+/verify.tsx
new file mode 100644
index 0000000..de6fbf4
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_auth+/verify.tsx
@@ -0,0 +1,144 @@
+import { getFormProps, getInputProps, useForm } from '@conform-to/react'
+import { getZodConstraint, parseWithZod } from '@conform-to/zod'
+import { type SEOHandle } from '@nasa-gcn/remix-seo'
+import { Form, useSearchParams } from 'react-router'
+import { HoneypotInputs } from 'remix-utils/honeypot/react'
+import { z } from 'zod'
+import { GeneralErrorBoundary } from 'remote/components/error-boundary'
+import { ErrorList, OTPField } from 'remote/components/forms'
+import { Spacer } from 'remote/components/spacer'
+import { StatusButton } from 'remote/components/ui/status-button'
+import { checkHoneypot } from '#app/utils/honeypot.server.ts'
+import { useIsPending } from '#app/utils/misc.tsx'
+import { type Route } from './+types/verify.ts'
+import { validateRequest } from './verify.server.ts'
+
+export const handle: SEOHandle = {
+ getSitemapEntries: () => null,
+}
+
+export const codeQueryParam = 'code'
+export const targetQueryParam = 'target'
+export const typeQueryParam = 'type'
+export const redirectToQueryParam = 'redirectTo'
+const types = ['onboarding', 'reset-password', 'change-email', '2fa'] as const
+const VerificationTypeSchema = z.enum(types)
+export type VerificationTypes = z.infer
+
+export const VerifySchema = z.object({
+ [codeQueryParam]: z.string().min(6).max(6),
+ [typeQueryParam]: VerificationTypeSchema,
+ [targetQueryParam]: z.string(),
+ [redirectToQueryParam]: z.string().optional(),
+})
+
+export async function action({ request }: Route.ActionArgs) {
+ const formData = await request.formData()
+ await checkHoneypot(formData)
+ return validateRequest(request, formData)
+}
+
+export default function VerifyRoute({ actionData }: Route.ComponentProps) {
+ const [searchParams] = useSearchParams()
+ const isPending = useIsPending()
+ const parseWithZoddType = VerificationTypeSchema.safeParse(
+ searchParams.get(typeQueryParam),
+ )
+ const type = parseWithZoddType.success ? parseWithZoddType.data : null
+
+ const checkEmail = (
+ <>
+ Check your email
+
+ We've sent you a code to verify your email address.
+
+ >
+ )
+
+ const headings: Record = {
+ onboarding: checkEmail,
+ 'reset-password': checkEmail,
+ 'change-email': checkEmail,
+ '2fa': (
+ <>
+ Check your 2FA app
+
+ Please enter your 2FA code to verify your identity.
+
+ >
+ ),
+ }
+
+ const [form, fields] = useForm({
+ id: 'verify-form',
+ constraint: getZodConstraint(VerifySchema),
+ lastResult: actionData?.result,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: VerifySchema })
+ },
+ defaultValue: {
+ code: searchParams.get(codeQueryParam),
+ type: type,
+ target: searchParams.get(targetQueryParam),
+ redirectTo: searchParams.get(redirectToQueryParam),
+ },
+ })
+
+ return (
+
+
+ {type ? headings[type] : 'Invalid Verification Type'}
+
+
+
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return
+}
diff --git a/examples/federation/epic-stack/app/routes/_marketing+/about.tsx b/examples/federation/epic-stack/app/routes/_marketing+/about.tsx
new file mode 100644
index 0000000..55ef96d
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_marketing+/about.tsx
@@ -0,0 +1,3 @@
+export default function AboutRoute() {
+ return About page
+}
diff --git a/examples/federation/epic-stack/app/routes/_marketing+/index.tsx b/examples/federation/epic-stack/app/routes/_marketing+/index.tsx
new file mode 100644
index 0000000..972e4a9
--- /dev/null
+++ b/examples/federation/epic-stack/app/routes/_marketing+/index.tsx
@@ -0,0 +1,101 @@
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from 'remote/components/ui/tooltip'
+import { cn } from '#app/utils/misc.tsx'
+import { type Route } from './+types/index.ts'
+import { logos } from './logos/logos.ts'
+
+export const meta: Route.MetaFunction = () => [{ title: 'Epic Notes' }]
+
+// Tailwind Grid cell classes lookup
+const columnClasses: Record<(typeof logos)[number]['column'], string> = {
+ 1: 'xl:col-start-1',
+ 2: 'xl:col-start-2',
+ 3: 'xl:col-start-3',
+ 4: 'xl:col-start-4',
+ 5: 'xl:col-start-5',
+}
+const rowClasses: Record<(typeof logos)[number]['row'], string> = {
+ 1: 'xl:row-start-1',
+ 2: 'xl:row-start-2',
+ 3: 'xl:row-start-3',
+ 4: 'xl:row-start-4',
+ 5: 'xl:row-start-5',
+ 6: 'xl:row-start-6',
+}
+
+export default function Index() {
+ return (
+
+
+
+
+
+ {logos.map((logo, i) => (
+
+
+
+