diff --git a/.env.example b/.env.example index fb3d6ea4..ad7ce014 100644 --- a/.env.example +++ b/.env.example @@ -4,10 +4,17 @@ DATABASE_URL=postgresql://devcard:devcard@localhost:5432/devcard?schema=public # ─── Redis ─── REDIS_URL=redis://localhost:6379 +# ─── Set The Url ─── +PUBLIC_APP_URL= + # ─── JWT ─── +# JWT_SECRET: any long random string, minimum 32 characters +# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" JWT_SECRET=your-super-secret-jwt-key-change-in-production # ─── Encryption (for OAuth tokens) ─── +# ENCRYPTION_KEY: must be exactly 32 bytes = 64 hex characters +# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" ENCRYPTION_KEY=your-32-byte-hex-encryption-key-here # ─── GitHub OAuth ─── @@ -25,4 +32,4 @@ MOBILE_REDIRECT_URI=devcard://oauth/callback # ─── Server ─── PORT=3000 -NODE_ENV=development +NODE_ENV=development \ No newline at end of file diff --git a/.github/scripts/ciScript.js b/.github/scripts/ciScript.js new file mode 100644 index 00000000..4e4e4792 --- /dev/null +++ b/.github/scripts/ciScript.js @@ -0,0 +1,81 @@ +const isTestFile = (file) => /\.(test|spec)\.[jt]sx?$/.test(file); + +const deriveTestFiles = (files) => { + return files.map((file) => { + if (isTestFile(file)) return file; + + const withoutExt = file.replace(/\.[jt]sx?$/, ''); + const parts = withoutExt.split('/'); + const baseName = parts[parts.length - 1]; + const dir = parts.slice(0, -1).join('/'); + + return `${dir}/__tests__/${baseName}.test.ts`; + }); +}; + +module.exports = async ({ github, context, core }) => { + const owner = context.repo.owner; + const repo = context.repo.repo; + const pr = context.payload.pull_request; + const prNumber = pr.number; + const prState = pr.state; + + const backendFiles = []; + const mobileFiles = []; + const webFiles = []; + + try { + if (prState === 'closed') { + console.log(`PR state is: ${prState}`); + return { + backendChanged: false, + mobileChanged: false, + webChanged: false + }; + } + + const changedFiles = await github.paginate( + github.rest.pulls.listFiles, + { + owner, + repo, + pull_number: prNumber + } + ); + + changedFiles.forEach((file) => { + const fileName = file.filename; + + if (fileName.startsWith('apps/backend/')) { + backendFiles.push(fileName); + } else if (fileName.startsWith('apps/mobile/')) { + mobileFiles.push(fileName); + } else if (fileName.startsWith('apps/web/')) { + webFiles.push(fileName); + } + }); + + const strippedBackend = backendFiles.map(f => f.replace('apps/backend/', '')); + const strippedMobile = mobileFiles.map(f => f.replace('apps/mobile/', '')); + + console.log({ backendFiles, mobileFiles, webFiles }); + + core.setOutput('backendFiles', strippedBackend.join(' ')); + core.setOutput('mobileFiles', strippedMobile.join(' ')); + core.setOutput('webFiles', webFiles.map(f => f.replace('apps/web/', '')).join(' ')); + core.setOutput('backendTestFiles', deriveTestFiles(strippedBackend).join(' ')); + core.setOutput('mobileTestFiles', deriveTestFiles(strippedMobile).join(' ')); + core.setOutput('backendChanged', backendFiles.length > 0); + core.setOutput('mobileChanged', mobileFiles.length > 0); + core.setOutput('webChanged', webFiles.length > 0); + + } catch (error) { + console.error(error); + + return { + backendChanged: false, + mobileChanged: false, + webChanged: false + }; + } +}; \ No newline at end of file diff --git a/.github/scripts/commentResults.js b/.github/scripts/commentResults.js new file mode 100644 index 00000000..a1a96089 --- /dev/null +++ b/.github/scripts/commentResults.js @@ -0,0 +1,100 @@ +module.exports = async ({ + github, + context, + backend, + mobile, + web, + backendLint, + backendTest, + backendTypecheck, + mobileLint, + mobileTest, + webCheck, + webBuild, + backendLintOutput, + mobileLintOutput, +}) => { + const owner = context.repo.owner; + const repo = context.repo.repo; + const prNumber = context.payload.pull_request.number; + + const status = (s) => { + if (s === 'success') return 'PASS'; + if (s === 'failure') return 'FAIL'; + if (s === 'skipped') return 'SKIP'; + return '-'; + }; + + const lintDetails = (output) => { + if (!output || !output.trim()) return ''; + return `\n
\nView lint errors\n\n\`\`\`\n${output.trim()}\n\`\`\`\n
`; + }; + + const anyFailure = [backend, mobile, web].includes('failure'); + const title = anyFailure ? 'CI — Checks Failed' : 'CI — All Checks Passed'; + const timestamp = new Date().toUTCString(); + + const body = `## ${title} + +### Backend — ${status(backend)} + +| Check | Result | +|---|---| +| Lint | ${status(backendLint)} | +| Test | ${status(backendTest)} | +| Typecheck | ${status(backendTypecheck)} | +${backendLint === 'failure' ? lintDetails(backendLintOutput) : ''} + +### Mobile — ${status(mobile)} + +| Check | Result | +|---|---| +| Lint | ${status(mobileLint)} | +| Test | ${status(mobileTest)} | +${mobileLint === 'failure' ? lintDetails(mobileLintOutput) : ''} + +### Web — ${status(web)} + +| Check | Result | +|---|---| +| Check | ${status(webCheck)} | +| Build | ${status(webBuild)} | + +--- +Last updated: \`${timestamp}\``; + + const COMMENT_MARKER = '## CI —'; + + try { + const comments = await github.paginate( + github.rest.issues.listComments, + { + owner, + repo, + issue_number: prNumber + } + ); + + const existing = comments.find( + c => c.body && c.body.startsWith(COMMENT_MARKER) + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: prNumber, + body + }); + } + } catch (err) { + console.error(err); + } +}; \ No newline at end of file diff --git a/.github/scripts/discordPinReminder.js b/.github/scripts/discordPinReminder.js new file mode 100644 index 00000000..d5724578 --- /dev/null +++ b/.github/scripts/discordPinReminder.js @@ -0,0 +1,37 @@ +module.exports = async ({ github, context }) => { + const pr = context.payload.pull_request; + const ignoreUsers = [ + 'ShantKhatri', + 'Harxhit', + 'blankirigaya' + ] + try { + // Only continue if merged + if (!pr || !pr.merged) { + console.log('PR not merged.'); + return; + } + + const prNumber = pr.number; + const contributor = pr.user.login; + + if(ignoreUsers.includes(contributor)){ + console.log(`Ignoring PR #${prNumber} by ${contributor}`); + return; + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: `Congratulations @${contributor} on getting PR #${prNumber} merged! + + Thank you for your contribution. Please mention @Harxhit in our Discord server to receive the appropriate GSSoC labels and recognition. + ` + }); + + console.log(`Comment added to PR #${prNumber}`); + } catch (error) { + console.error(error) + } +}; diff --git a/.github/scripts/unassignIssues.js b/.github/scripts/unassignIssues.js new file mode 100644 index 00000000..b4886e91 --- /dev/null +++ b/.github/scripts/unassignIssues.js @@ -0,0 +1,177 @@ +module.exports = async ({ github, context }) => { + const owner = context.repo.owner; + const repo = context.repo.repo; + + const PROTECTED_ASSIGNEES = [ + 'ShantKhatri', + 'Harxhit', + 'blankirigaya' + ]; + + // Fetch all open issues (excluding PRs) + let page = 1; + let issues = []; + + while (true) { + const { data } = await github.rest.issues.listForRepo({ + owner, + repo, + state: 'open', + per_page: 100, + page, + }); + + const onlyIssues = data.filter( + item => !item.pull_request + ); + + issues = issues.concat(onlyIssues); + + if (data.length < 100) break; + page++; + } + + console.log( + `Found ${issues.length} open issue(s) to check.` + ); + + for (const issue of issues) { + const issueNumber = issue.number; + + // Skip if no assignees + if ( + !issue.assignees || + issue.assignees.length === 0 + ) { + console.log( + `Issue #${issueNumber} has no assignees — skipping.` + ); + continue; + } + + const assigneeLogins = + issue.assignees.map(a => a.login); + + // Skip protected assignees + const hasProtectedAssignee = + assigneeLogins.some(login => + PROTECTED_ASSIGNEES.includes(login) + ); + + if (hasProtectedAssignee) { + console.log( + `Issue #${issueNumber} has protected assignee(s) — skipping.` + ); + continue; + } + + let linkedPRFound = false; + let assignedAt = null; + + try { + const timeline = + await github.rest.issues.listEventsForTimeline({ + owner, + repo, + issue_number: issueNumber, + per_page: 100, + }); + + // Check linked PR + linkedPRFound = timeline.data.some(event => { + const pr = event.source?.issue; + + return ( + event.event === 'cross-referenced' && + pr?.pull_request && + pr.state === 'open' + ); + }); + + // Find latest assignment event + const assignEvents = + timeline.data.filter( + event => event.event === 'assigned' + ); + + if (assignEvents.length > 0) { + assignedAt = + assignEvents[ + assignEvents.length - 1 + ].created_at; + } + } catch (err) { + console.log( + `Could not fetch timeline for issue #${issueNumber}: ${err.message}` + ); + continue; + } + + // Skip if no assignment timestamp + if (!assignedAt) { + console.log( + `Issue #${issueNumber} has no assignment timestamp — skipping.` + ); + continue; + } + + const assignedDate = + new Date(assignedAt); + const now = new Date(); + + const daysAssigned = + (now - assignedDate) / + (1000 * 60 * 60 * 24); + + console.log( + `Issue #${issueNumber} assigned for ${daysAssigned.toFixed( + 1 + )} day(s).` + ); + + // Skip if assigned <= 5 days + if (daysAssigned <= 5) { + console.log( + `Issue #${issueNumber} assigned less than 5 days ago — skipping.` + ); + continue; + } + + // Skip if linked PR exists + if (linkedPRFound) { + console.log( + `Issue #${issueNumber} has linked open/draft PR — keeping assignment.` + ); + continue; + } + + // Remove assignees + await github.rest.issues.removeAssignees({ + owner, + repo, + issue_number: issueNumber, + assignees: assigneeLogins, + }); + + const assigneesMention = + assigneeLogins + .map(user => `@${user}`) + .join(', '); + + // Comment + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body: `Hey @ShantKhatri (Project Admin) and @Harxhit (Maintainer), + +This issue (previously assigned to ${assigneesMention}) has been **automatically unassigned** because no linked pull request was found within 5 days of assignment. + +If work is in progress, please open and link a PR to keep the assignment active.`, + }); + + console.log( + `Issue #${issueNumber} unassigned successfully.` + ); + } +}; diff --git a/.github/scripts/welcomeScript.js b/.github/scripts/welcomeScript.js new file mode 100644 index 00000000..aa48ce7b --- /dev/null +++ b/.github/scripts/welcomeScript.js @@ -0,0 +1,72 @@ +module.exports = async ({ github, context }) => { + const owner = context.repo.owner; + const repo = context.repo.repo; + const issueNumber = context.issue.number; + const eventName = context.eventName; + const ghUsername = context.payload.sender.login; + + try { + const issueAssociation = + context.payload.issue?.author_association; + + if ( + eventName === 'issues' && + issueAssociation === 'NONE' + ) { + // Verify this is truly their first issue (listForRepo returns PRs too) + const userIssues = await github.rest.issues.listForRepo({ + owner, + repo, + state: 'all', + creator: ghUsername, + per_page: 10 + }); + + const actualIssues = userIssues.data.filter(issue => !issue.pull_request); + + if (actualIssues.length === 1) { + return await github.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body: `👋 Thanks for opening your first issue, @${ghUsername}! + +We appreciate your contribution and are excited to have you here. Please make sure to follow the contribution guidelines and provide as much detail as possible. + +To stay updated, ask questions, and connect with maintainers and contributors, please join our Discord community: +https://discord.gg/QueQN83wn + +Looking forward to collaborating with you!` + }); + } + } + + const prAssociation = + context.payload.pull_request?.author_association; + + if ( + eventName === 'pull_request_target' && + ( + prAssociation === 'FIRST_TIMER' || + prAssociation === 'FIRST_TIME_CONTRIBUTOR' + ) + ) { + return await github.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body: `🎉 Thanks for your first contribution, @${ghUsername}! + +We're excited to have you here. A maintainer will review your PR soon. Please check CI results and review any feedback if needed. + +To stay updated, ask questions, and connect with maintainers and contributors, please join our Discord community: +https://discord.gg/QueQN83wn + +Looking forward to collaborating with you!` + }); + } + + } catch (error) { + console.error(error); + } +}; \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..fbb952e7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,192 @@ +name: CI + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + pull-requests: write + +jobs: + detect-changes: + runs-on: ubuntu-latest + + outputs: + backendChanged: ${{ steps.detect.outputs.backendChanged }} + mobileChanged: ${{ steps.detect.outputs.mobileChanged }} + webChanged: ${{ steps.detect.outputs.webChanged }} + backendFiles: ${{ steps.detect.outputs.backendFiles }} + mobileFiles: ${{ steps.detect.outputs.mobileFiles }} + webFiles: ${{ steps.detect.outputs.webFiles }} + backendTestFiles: ${{ steps.detect.outputs.backendTestFiles }} + mobileTestFiles: ${{ steps.detect.outputs.mobileTestFiles }} + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.event.pull_request.head.sha }} + + + - name: Detect changed files + id: detect + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const script = require('./.github/scripts/ciScript.js'); + return await script({ github, context, core }); + + backend-ci: + needs: detect-changes + if: needs.detect-changes.outputs.backendChanged == 'true' + runs-on: ubuntu-latest + + outputs: + lint_result: ${{ steps.backend_lint.outcome }} + test_result: ${{ steps.backend_test.outcome }} + typecheck_result: ${{ steps.backend_typecheck.outcome }} + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.event.pull_request.head.sha }} + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e + with: + node-version: 22 + + - uses: pnpm/action-setup@v6.0.8 + + - run: pnpm install + + - name: Backend lint + id: backend_lint + continue-on-error: true + run: cd apps/backend && pnpm eslint ${{ needs.detect-changes.outputs.backendFiles }} + + - name: Backend test + id: backend_test + if: needs.detect-changes.outputs.backendTestFiles != '' + continue-on-error: true + run: cd apps/backend && pnpm test --passWithNoTests ${{ needs.detect-changes.outputs.backendTestFiles }} + + - name: Backend typecheck + id: backend_typecheck + continue-on-error: true + run: cd apps/backend && pnpm typecheck + + - name: Fail job if any check failed + if: > + steps.backend_lint.outcome == 'failure' || + steps.backend_test.outcome == 'failure' || + steps.backend_typecheck.outcome == 'failure' + run: exit 1 + + web-ci: + needs: detect-changes + if: needs.detect-changes.outputs.webChanged == 'true' + runs-on: ubuntu-latest + + outputs: + check_result: ${{ steps.web_check.outcome }} + build_result: ${{ steps.web_build.outcome }} + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.event.pull_request.head.sha }} + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e + with: + node-version: 22 + + - uses: pnpm/action-setup@v6.0.8 + + - run: pnpm install + + - name: Web check + id: web_check + continue-on-error: true + run: cd apps/web && pnpm check + + - name: Web build + id: web_build + continue-on-error: true + run: cd apps/web && pnpm build + + - name: Fail job if any check failed + if: > + steps.web_check.outcome == 'failure' || + steps.web_build.outcome == 'failure' + run: exit 1 + + mobile-ci: + needs: detect-changes + if: needs.detect-changes.outputs.mobileChanged == 'true' + runs-on: ubuntu-latest + + outputs: + lint_result: ${{ steps.mobile_lint.outcome }} + test_result: ${{ steps.mobile_test.outcome }} + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.event.pull_request.head.sha }} + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e + with: + node-version: 22 + + - uses: pnpm/action-setup@v6.0.8 + + - run: pnpm install + + - name: Mobile lint + id: mobile_lint + continue-on-error: true + run: cd apps/mobile && pnpm eslint ${{ needs.detect-changes.outputs.mobileFiles }} + + - name: Mobile test + id: mobile_test + if: needs.detect-changes.outputs.mobileTestFiles != '' + continue-on-error: true + run: cd apps/mobile && pnpm test --passWithNoTests ${{ needs.detect-changes.outputs.mobileTestFiles }} + + - name: Fail job if any check failed + if: > + steps.mobile_lint.outcome == 'failure' || + steps.mobile_test.outcome == 'failure' + run: exit 1 + + comment-results: + needs: + - backend-ci + - web-ci + - mobile-ci + if: always() + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Comment results + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const script = require('./.github/scripts/commentResults.js'); + await script({ + github, + context, + backend: '${{ needs.backend-ci.result }}', + web: '${{ needs.web-ci.result }}', + mobile: '${{ needs.mobile-ci.result }}', + backendLint: '${{ needs.backend-ci.outputs.lint_result }}', + backendTest: '${{ needs.backend-ci.outputs.test_result }}', + backendTypecheck: '${{ needs.backend-ci.outputs.typecheck_result }}', + webCheck: '${{ needs.web-ci.outputs.check_result }}', + webBuild: '${{ needs.web-ci.outputs.build_result }}', + mobileLint: '${{ needs.mobile-ci.outputs.lint_result }}', + mobileTest: '${{ needs.mobile-ci.outputs.test_result }}', + }); \ No newline at end of file diff --git a/.github/workflows/gssoc-discord-pin-reminder.yml b/.github/workflows/gssoc-discord-pin-reminder.yml new file mode 100644 index 00000000..5c6cd7cb --- /dev/null +++ b/.github/workflows/gssoc-discord-pin-reminder.yml @@ -0,0 +1,27 @@ +name: GSSoC Discord Pin Reminder + +on: + pull_request_target: + types: [closed] + workflow_dispatch: + +jobs: + discord-pin-reminder: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + + permissions: + pull-requests: write + issues: write + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Notify contributor about Discord GSSoC labels + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const script = require('./.github/scripts/discordPinReminder.js'); + await script({ github, context }); diff --git a/.github/workflows/unassign-unlinked-issues.yml b/.github/workflows/unassign-unlinked-issues.yml new file mode 100644 index 00000000..40bcab5f --- /dev/null +++ b/.github/workflows/unassign-unlinked-issues.yml @@ -0,0 +1,24 @@ +name: Unassign Issues Without Linked PR + +on: + schedule: + - cron: '0 9 */5 * *' # Runs every 5 days at 9:00 AM UTC + workflow_dispatch: # Also allows manual triggering + +jobs: + unassign-issues: + runs-on: ubuntu-latest + permissions: + issues: write + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + + - name: Unassign issues with no linked PR and notify + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const script = require('./.github/scripts/unassignIssues.js'); + await script({ github, context }); \ No newline at end of file diff --git a/.github/workflows/welcome-first-time.yml b/.github/workflows/welcome-first-time.yml new file mode 100644 index 00000000..2f3acc4e --- /dev/null +++ b/.github/workflows/welcome-first-time.yml @@ -0,0 +1,27 @@ +name: Welcome First-Time Contributors + +on: + issues: + types: [opened] + pull_request_target: + types: [opened] + +permissions: + issues: write + pull-requests: write + +jobs: + welcome: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + + - name: Welcome first-time contributor + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const script = require('./.github/scripts/welcomeScript.js'); + await script({ github, context }); \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00cb1e8b..0f95620b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,12 @@ # Contributing to DevCard -Thank you for your interest in contributing to DevCard! This guide will help you get started. +

+ + Discord Server + +

+ +**Join the community** — ask questions, get help, discuss ideas, and meet other contributors on our [Discord server](https://discord.gg/QueQN83wn). ## Development Setup diff --git a/README.md b/README.md index cbe700ae..26261279 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ GitHub Repo + + Discord Server +

@@ -70,6 +73,11 @@ docker compose up -d # Copy environment config cp .env.example .env +# ⚠️ Replace secret placeholders before starting the server: +# JWT_SECRET → node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +# ENCRYPTION_KEY → node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +# Paste the generated values into your .env file. Never use placeholders in production. + # Run database migrations pnpm db:migrate @@ -270,6 +278,32 @@ New to open source? We've got you covered! Check out our [Good First Issues](htt See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions, coding standards, and PR process. +## Contributors + +Thanks to all the amazing people who contribute to **DevCard** 🚀 + +

+ + Contributors + +

+ +
+ +## Project Support + +

+ + Stars + +    + + Forks + +

+ +--- + ## License DevCard is licensed under the [Apache License 2.0](./LICENSE). diff --git a/apps/backend/README.md b/apps/backend/README.md new file mode 100644 index 00000000..a807f250 --- /dev/null +++ b/apps/backend/README.md @@ -0,0 +1,28 @@ +# DevCard Backend + +## Follow Engine Architecture + +DevCard implements a multi-layered Hybrid Follow Engine designed to connect platform professionals seamlessly while maintaining platform policy compliance. + +```mermaid +graph TD + A[User triggers Follow/Connect] --> B{Check Platform Strategy} + B -- api (GitHub) --> C[Layer 1: Direct OAuth API integration] + B -- webview (LinkedIn) --> D[Layer 2: In-app WebView Interaction Engine] + B -- link (GitLab/Devfolio) --> E[Layer 3: Native deep-linking / Browser redirect] + B -- copy (Discord) --> F[Layer 4: Clipboard Copy fallback] +``` + +### Layer 2: WebView Interaction Engine (LinkedIn) + +Due to LinkedIn's modern API restrictions preventing programmatic connection requests, direct API follow (Layer 1) is not viable. Instead, the WebView Interaction Engine routes the action through a secure, native WebView: + +1. **Routing Strategy**: The backend parses the connection request and returns `{ strategy: 'webview', url }` containing the resolved profile link. +2. **Session Persistence**: The mobile WebView loads the target profile URL using system-level OAuth cookie-sharing (`sharedCookiesEnabled={true}`), ensuring the user remains authenticated. +3. **DOM Introspection**: A lightweight JavaScript snippet is injected to continuously poll for the native LinkedIn 'Connect' button, smooth-scrolls it into view, and highlights it visually to encourage action. +4. **Interactive Send**: Users retain full control over actual connection request submission, adhering completely to platform terms of service. +5. **State Detection**: + - URL State Polling: The engine inspects URL transitions containing `invite-sent` or similar sub-routes. + - DOM Observation: The injected Javascript queries for structural indicators of successful invitation (e.g. "Pending" button state or toaster text) and posts a serialized message back to the native layer. +6. **Robust Fallback**: If network or WebView loading times out (>10s), the engine gracefully falls back to native deep links (`linkedin://profile?id={username}`) or launches the default browser with an interactive custom in-app overlay. +7. **Telemetry Logging**: Upon client-side success (detected via state changes or DOM indicators), the mobile app makes a `POST /api/follow/:platform/:targetUsername/log` request to the backend. This writes a record to the `FollowLog` database table for auditing and analytics tracking. diff --git a/apps/backend/eslint.config.js b/apps/backend/eslint.config.js new file mode 100644 index 00000000..3924db19 --- /dev/null +++ b/apps/backend/eslint.config.js @@ -0,0 +1,202 @@ +import tseslint from 'typescript-eslint'; +import pluginN from 'eslint-plugin-n'; +import pluginImportX from 'eslint-plugin-import-x'; +import pluginPromise from 'eslint-plugin-promise'; +import pluginSecurity from 'eslint-plugin-security'; +import pluginUnicorn from 'eslint-plugin-unicorn'; + +export default tseslint.config( + + // ─── Global Ignores ────────────────────────────────────────────────────────── + { + ignores: [ + 'dist/**', + 'build/**', + 'node_modules/**', + 'coverage/**', + 'prisma/migrations/**', + '**/*.d.ts', + ], + }, + + // ─── Base: ESLint Recommended + TypeScript ────────────────────────────────── + ...tseslint.configs.recommendedTypeChecked, + + // ─── Main Config ──────────────────────────────────────────────────────────── + { + files: ['src/**/*.ts'], + + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + + plugins: { + n: pluginN, + 'import-x': pluginImportX, + promise: pluginPromise, + security: pluginSecurity, + unicorn: pluginUnicorn, + }, + + settings: { + 'import-x/resolver': { + typescript: { project: './tsconfig.json' }, + node: true, + }, + node: { version: '>=18.0.0' }, + }, + + rules: { + + // ── TypeScript: Type Safety: currently off ───────────────────────────────────────────── + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-unnecessary-type-assertion': 'off', + '@typescript-eslint/prefer-nullish-coalescing': 'off', + '@typescript-eslint/prefer-optional-chain': 'off', + '@typescript-eslint/strict-boolean-expressions': 'off', + + // ── TypeScript: Async / Promises: currently off ──────────────────────────────────────── + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/no-misused-promises': 'off', + '@typescript-eslint/await-thenable': 'off', + '@typescript-eslint/require-await': 'off', + '@typescript-eslint/return-await': 'off', + + // ── TypeScript: Imports ───────────────────────────────────────────────── + '@typescript-eslint/consistent-type-imports': [ + 'error', + { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, + ], + '@typescript-eslint/consistent-type-exports': 'error', + '@typescript-eslint/no-import-type-side-effects': 'error', + + // ── TypeScript: Code Quality ──────────────────────────────────────────── + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/explicit-function-return-type': [ + 'warn', + { + allowExpressions: true, + allowTypedFunctionExpressions: true, + }, + ], + '@typescript-eslint/prefer-as-const': 'error', + '@typescript-eslint/no-redundant-type-constituents': 'warn', + '@typescript-eslint/no-shadow': 'error', + '@typescript-eslint/no-use-before-define': ['error', { functions: false }], + + // ── Node.js ───────────────────────────────────────────────────────────── + 'n/no-deprecated-api': 'error', + 'n/no-extraneous-import': 'error', + 'n/no-process-exit': 'off', + 'n/prefer-global/buffer': ['error', 'always'], + 'n/prefer-global/process': ['error', 'always'], + 'n/prefer-promises/fs': 'error', + 'n/prefer-promises/dns': 'error', + 'n/no-sync': 'warn', + + // ── Imports (import-x) ────────────────────────────────────────────────── + 'import-x/no-duplicates': 'error', + 'import-x/no-cycle': 'off', + 'import-x/no-self-import': 'error', + 'import-x/first': 'error', + 'import-x/newline-after-import': 'error', + 'import-x/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'type'], + 'newlines-between': 'always', + alphabetize: { order: 'asc', caseInsensitive: true }, + }, + ], + + // ── Promises ──────────────────────────────────────────────────────────── + 'promise/always-return': 'off', + 'promise/catch-or-return': 'off', + 'promise/no-new-statics': 'error', + 'promise/no-return-wrap': 'error', + 'promise/param-names': 'error', + 'promise/no-promise-in-callback': 'warn', + + // ── Security ──────────────────────────────────────────────────────────── + 'security/detect-object-injection': 'off', + 'security/detect-non-literal-regexp': 'warn', + 'security/detect-non-literal-fs-filename': 'warn', + 'security/detect-eval-with-expression': 'error', + 'security/detect-child-process': 'warn', + 'security/detect-possible-timing-attacks': 'warn', + + // ── Unicorn ───────────────────────────────────────────────────────────── + 'unicorn/prefer-node-protocol': 'error', + 'unicorn/no-process-exit': 'off', + 'unicorn/error-message': 'off', + 'unicorn/throw-new-error': 'off', + 'unicorn/no-useless-undefined': 'off', + 'unicorn/prefer-string-slice': 'warn', + 'unicorn/no-for-loop': 'off', + 'unicorn/prefer-includes': 'warn', + 'unicorn/no-array-for-each': 'off', + 'unicorn/prefer-ternary': 'off', + 'unicorn/prevent-abbreviations': 'off', + + // ── Core ESLint ───────────────────────────────────────────────────────── + 'no-console': 'warn', + 'eqeqeq': ['error', 'always'], + 'no-var': 'error', + 'prefer-const': 'error', + 'no-throw-literal': 'error', + 'curly': ['error', 'all'], + 'object-shorthand': 'error', + 'no-lonely-if': 'warn', + 'no-nested-ternary': 'off', + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'no-param-reassign': [ + 'error', + { + props: true, + ignorePropertyModificationsFor: ['acc', 'request', 'reply'], + }, + ], + }, + }, + + // ─── Test File Overrides ──────────────────────────────────────────────────── + { + files: ['**/*.test.ts', '**/*.spec.ts', 'src/__tests__/**/*.ts'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-floating-promises': 'off', + 'security/detect-object-injection': 'off', + 'no-console': 'off', + }, + }, + + // ─── Prisma Seed / Scripts Override ──────────────────────────────────────── + { + files: ['prisma/**/*.ts', 'scripts/**/*.ts'], + rules: { + 'n/no-process-exit': 'off', + 'unicorn/no-process-exit': 'off', + 'no-console': 'off', + }, + }, +); \ No newline at end of file diff --git a/apps/backend/package.json b/apps/backend/package.json index b8d11411..8bc19bf8 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -9,11 +9,13 @@ "start": "node dist/server.js", "test": "vitest run", "test:watch": "vitest", + "lint:fix": "eslint src/ --fix", "lint": "eslint src/", "db:migrate": "prisma migrate dev", "db:deploy": "prisma migrate deploy", "db:seed": "tsx prisma/seed.ts", - "db:studio": "prisma studio" + "db:studio": "prisma studio", + "typecheck": "tsc --noEmit" }, "dependencies": { "@devcard/shared": "workspace:*", @@ -22,6 +24,7 @@ "@fastify/helmet": "^12.0.0", "@fastify/jwt": "^9.0.0", "@fastify/multipart": "^9.0.0", + "@fastify/rate-limit": "^10.3.0", "@fastify/static": "^8.0.0", "@prisma/client": "^6.0.0", "dotenv": "^16.4.0", @@ -34,10 +37,18 @@ "devDependencies": { "@types/node": "^22.0.0", "@types/qrcode": "^1.5.0", + "eslint": "^10.4.0", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import-x": "^4.16.2", + "eslint-plugin-n": "^18.0.1", + "eslint-plugin-promise": "^7.3.0", + "eslint-plugin-security": "^4.0.0", + "eslint-plugin-unicorn": "^64.0.0", "pino-pretty": "^13.1.3", "prisma": "^6.0.0", "tsx": "^4.0.0", "typescript": "^5.4.0", + "typescript-eslint": "^8.59.3", "vitest": "^2.0.0" } -} +} \ No newline at end of file diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 13dec572..28458021 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -1,10 +1,10 @@ generator client { provider = "prisma-client-js" } - datasource db { provider = "postgresql" url = env("DATABASE_URL") + } model User { @@ -29,6 +29,11 @@ model User { ownedViews CardView[] @relation("cardOwner") viewedCards CardView[] @relation("cardViewer") followLogs FollowLog[] + organizer Event[] + attendedEvents EventAttendee[] + + ownedTeams Team[] @relation("TeamOwner") + teamMemberships TeamMember[] @relation("TeamMember") @@unique([provider, providerId]) @@map("users") @@ -124,3 +129,69 @@ model FollowLog { @@map("follow_logs") } + +model Event { + id String @id @default(uuid()) + name String + slug String @unique + location String + description String? + organizerId String + startDate DateTime + endDate DateTime + isPublic Boolean @default(true) + createdAt DateTime @default(now()) @map("created_at") + attendees EventAttendee[] + + organizer User @relation(fields: [organizerId], references: [id]) +} + +model EventAttendee { + id String @id @default(uuid()) + userId String + eventId String + joinedAt DateTime + + event Event @relation(fields: [eventId] , references: [id]) + user User @relation(fields: [userId],references: [id]) + + @@unique([userId, eventId]) +} + +enum TeamRole { + OWNER + ADMIN + MEMBER +} + +model Team{ + id String @id @default(uuid()) + name String + slug String @unique + description String? + avatarUrl String? + ownerId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + owner User @relation("TeamOwner", fields: [ownerId], references: [id], onDelete: Restrict) + members TeamMember[] @relation("TeamMember") + + @@map("teams") + @@index([slug]) +} + +model TeamMember{ + id String @id @default(uuid()) + teamId String + userId String + role TeamRole + joinedAt DateTime + + team Team @relation("TeamMember",fields: [teamId] , references: [id], onDelete: Cascade) + user User @relation("TeamMember",fields: [userId] , references: [id]) + + @@unique([userId, teamId]) + @@index([userId]) + @@map("team_members") +} \ No newline at end of file diff --git a/apps/backend/prisma/seed.ts b/apps/backend/prisma/seed.ts index a799a3e3..f19345d8 100644 --- a/apps/backend/prisma/seed.ts +++ b/apps/backend/prisma/seed.ts @@ -142,8 +142,8 @@ async function main() { } main() - .catch((e) => { - console.error('❌ Seed failed:', e); + .catch((error) => { + console.error('❌ Seed failed:', error); process.exit(1); }) .finally(async () => { diff --git a/apps/backend/src/__tests__/analytics.test.ts b/apps/backend/src/__tests__/analytics.test.ts new file mode 100644 index 00000000..4f0d07ae --- /dev/null +++ b/apps/backend/src/__tests__/analytics.test.ts @@ -0,0 +1,466 @@ +import { + describe, + it, + expect, + beforeEach, + afterEach, + vi, +} from 'vitest'; + +import Fastify, { + type FastifyInstance, +} from 'fastify'; + +import type { PrismaClient } from '@prisma/client'; + +import { analyticsRoutes } from '../routes/analytics'; + +// ─── Shared mock data ──────────────────────────────────────────────────────── + +const MOCK_USER_ID = 'user-001'; + +// ─── Prisma mock ───────────────────────────────────────────────────────────── + +const prismaMock = { + cardView: { + count: vi.fn(), + findMany: vi.fn(), + groupBy: vi.fn(), + }, + followLog: { + count: vi.fn(), + }, +}; + +// ─── App factory ───────────────────────────────────────────────────────────── + +let mockJwtVerify = vi.fn(); + +async function buildApp(): Promise { + const app = Fastify({ + logger: false, + }); + + app.decorate( + 'prisma', + prismaMock as unknown as PrismaClient + ); + + app.decorateRequest( + 'jwtVerify', + function () { + return mockJwtVerify(); + } + ); + + app.decorate( + 'authenticate', + async function ( + request: any, + reply: any + ) { + try { + const user = + await request.jwtVerify(); + + request.user = user; + } catch (_err) { + return reply.status(401).send({ + error: 'Unauthorized', + }); + } + } + ); + + await app.register( + analyticsRoutes, + { + prefix: '/api/analytics', + } + ); + + await app.ready(); + return app; +} + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +function authHeader(): Record { + return { + Authorization: + 'Bearer mock-token', + }; +} + +// ─── Test Suite ────────────────────────────────────────────────────────────── + +describe( + 'Analytics API', + () => { + let app: FastifyInstance; + + beforeEach( + async () => { + vi.clearAllMocks(); + + mockJwtVerify.mockResolvedValue( + { + id: MOCK_USER_ID, + } + ); + + app = await buildApp(); + } + ); + + afterEach( + async () => { + await app.close(); + } + ); + + // ── GET /overview ─────────────────────────────────────────────────────── + + describe( + 'GET /api/analytics/overview', + () => { + it( + '200 — returns analytics overview', + async () => { + prismaMock.cardView.count + .mockResolvedValueOnce( + 100 + ) + .mockResolvedValueOnce( + 10 + ); + + prismaMock.followLog.count.mockResolvedValue( + 5 + ); + + prismaMock.cardView.findMany.mockResolvedValue( + [ + { + id: 'view-1', + viewer: { + displayName: + 'John', + avatarUrl: + null, + }, + card: { + title: + 'My Card', + }, + }, + ] + ); + + prismaMock.cardView.groupBy.mockResolvedValue( + [ + { + viewerId: + 'u1', + viewerIp: + null, + }, + { + viewerId: + 'u2', + viewerIp: + null, + }, + ] + ); + + const res = + await app.inject( + { + method: + 'GET', + url: + '/api/analytics/overview', + headers: + authHeader(), + } + ); + + expect( + res.statusCode + ).toBe(200); + + const body = + res.json(); + + expect( + body.totalViews + ).toBe(100); + + expect( + body.viewsToday + ).toBe(10); + + expect( + body.totalFollows + ).toBe(5); + + expect( + body.uniqueViewers + ).toBe(2); + + expect( + body.recentViews + ).toHaveLength( + 1 + ); + } + ); + + it( + '401 — rejects unauthenticated request', + async () => { + mockJwtVerify.mockRejectedValue( + new Error( + 'Unauthorized' + ) + ); + + const res = + await app.inject( + { + method: + 'GET', + url: + '/api/analytics/overview', + } + ); + + expect( + res.statusCode + ).toBe(401); + + expect( + res.json() + ).toMatchObject( + { + error: + 'Unauthorized', + } + ); + } + ); + } + ); + + // ── GET /views ────────────────────────────────────────────────────────── + + describe( + 'GET /api/analytics/views', + () => { + it( + '200 — returns paginated views', + async () => { + prismaMock.cardView.count.mockResolvedValue( + 45 + ); + + prismaMock.cardView.findMany.mockResolvedValue( + [ + { + id: + 'view-1', + viewer: + { + id: + 'viewer-1', + username: + 'john', + displayName: + 'John', + avatarUrl: + null, + }, + card: + { + id: + 'card-1', + title: + 'Portfolio', + }, + }, + ] + ); + + const res = + await app.inject( + { + method: + 'GET', + url: + '/api/analytics/views?page=2', + headers: + authHeader(), + } + ); + + expect( + res.statusCode + ).toBe(200); + + const body = + res.json(); + + expect( + body.data + ).toHaveLength( + 1 + ); + + expect( + body.meta + ).toMatchObject( + { + total: + 45, + page: 2, + limit: + 20, + totalPages: + 3, + } + ); + + expect( + prismaMock.cardView.findMany.mock.calls[0][0] + ).toMatchObject( + { + skip: + 20, + take: + 20, + } + ); + } + ); + + it( + '200 — filters by cardId when provided', + async () => { + prismaMock.cardView.count.mockResolvedValue( + 0 + ); + + prismaMock.cardView.findMany.mockResolvedValue( + [] + ); + + const res = + await app.inject( + { + method: + 'GET', + url: + '/api/analytics/views?cardId=card-123', + headers: + authHeader(), + } + ); + + expect( + res.statusCode + ).toBe(200); + + expect( + prismaMock.cardView.count.mock.calls[0][0] + ).toMatchObject( + { + where: + { + ownerId: + MOCK_USER_ID, + cardId: + 'card-123', + }, + } + ); + } + ); + + it( + '200 — defaults to page 1', + async () => { + prismaMock.cardView.count.mockResolvedValue( + 0 + ); + + prismaMock.cardView.findMany.mockResolvedValue( + [] + ); + + const res = + await app.inject( + { + method: + 'GET', + url: + '/api/analytics/views', + headers: + authHeader(), + } + ); + + expect( + res.statusCode + ).toBe(200); + + expect( + prismaMock.cardView.findMany.mock.calls[0][0] + ).toMatchObject( + { + skip: + 0, + take: + 20, + } + ); + } + ); + + it( + '401 — rejects unauthenticated request', + async () => { + mockJwtVerify.mockRejectedValue( + new Error( + 'Unauthorized' + ) + ); + + const res = + await app.inject( + { + method: + 'GET', + url: + '/api/analytics/views', + } + ); + + expect( + res.statusCode + ).toBe(401); + + expect( + res.json() + ).toMatchObject( + { + error: + 'Unauthorized', + } + ); + } + ); + } + ); + } +); \ No newline at end of file diff --git a/apps/backend/src/__tests__/app.test.ts b/apps/backend/src/__tests__/app.test.ts new file mode 100644 index 00000000..648d98a6 --- /dev/null +++ b/apps/backend/src/__tests__/app.test.ts @@ -0,0 +1,20 @@ +process.env.NODE_ENV = 'test'; + +import { describe, it, expect } from 'vitest'; +import { buildApp } from '../app'; + +describe('GET /health', () => { + it('should return status ok', async () => { + const app = await buildApp(); + + const res = await app.inject({ + method: 'GET', + url: '/health', + }); + + expect(res.statusCode).toBe(200); + expect(JSON.parse(res.body)).toEqual({ status: 'ok' }); + + await app.close(); + }); +}); \ No newline at end of file diff --git a/apps/backend/src/__tests__/cards.test.ts b/apps/backend/src/__tests__/cards.test.ts new file mode 100644 index 00000000..813883e8 --- /dev/null +++ b/apps/backend/src/__tests__/cards.test.ts @@ -0,0 +1,440 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import Fastify from 'fastify'; +import { cardRoutes } from '../routes/cards.js'; + +const USER_ID = 'user-123'; +const CARD_ID = 'card-abc'; +// Must be valid UUIDs — createCardSchema and updateCardSchema use z.string().uuid() +const OWNED_LINK_ID = '11111111-1111-1111-1111-111111111111'; +const FOREIGN_LINK_ID = '22222222-2222-2222-2222-222222222222'; + +const mockCard = { + id: CARD_ID, + userId: USER_ID, + title: 'My Card', + isDefault: true, + createdAt: new Date(), + updatedAt: new Date(), + cardLinks: [], +}; + +// $transaction executes the callback synchronously against the same mock client, +// mirroring Prisma's interactive-transactions API without a real DB connection. +const mockPrisma = { + card: { + count: vi.fn(), + create: vi.fn(), + findMany: vi.fn(), + findFirst: vi.fn(), + findUnique: vi.fn(), + update: vi.fn(), + updateMany: vi.fn(), + delete: vi.fn(), + }, + cardLink: { + deleteMany: vi.fn(), + createMany: vi.fn(), + }, + platformLink: { + findMany: vi.fn(), + }, + $transaction: vi.fn(), +}; + +// Re-wire $transaction before every test so that it executes the callback +// against the same mock client, preserving existing per-operation mocks. +function wireTransaction() { + mockPrisma.$transaction.mockImplementation( + async (callback: (tx: typeof mockPrisma) => Promise) => callback(mockPrisma), + ); +} + +async function buildApp() { + const app = Fastify({ logger: false }); + app.decorate('prisma', mockPrisma); + app.decorate('authenticate', async (request: any) => { + request.user = { id: USER_ID }; + }); + app.register(cardRoutes, { prefix: '/api/cards' }); + await app.ready(); + return app; +} + +// ───────────────────────────────────────────────────────────────────────────── +// POST /api/cards +// ───────────────────────────────────────────────────────────────────────────── + +describe('POST /api/cards — link ownership validation', () => { + beforeEach(() => { + vi.clearAllMocks(); + wireTransaction(); + }); + + it('returns 403 when a supplied linkId belongs to another user', async () => { + mockPrisma.platformLink.findMany.mockResolvedValue([]); + + const app = await buildApp(); + const res = await app.inject({ + method: 'POST', + url: '/api/cards', + payload: { title: 'Test Card', linkIds: [FOREIGN_LINK_ID] }, + }); + + expect(res.statusCode).toBe(403); + expect(res.json().error).toBe('One or more links do not belong to your account'); + expect(mockPrisma.card.create).not.toHaveBeenCalled(); + }); + + it('returns 403 when a mix of owned and foreign linkIds is supplied', async () => { + // Only 1 of 2 requested IDs is owned — count mismatch triggers 403 + mockPrisma.platformLink.findMany.mockResolvedValue([{ id: OWNED_LINK_ID }]); + + const app = await buildApp(); + const res = await app.inject({ + method: 'POST', + url: '/api/cards', + payload: { title: 'Test Card', linkIds: [OWNED_LINK_ID, FOREIGN_LINK_ID] }, + }); + + expect(res.statusCode).toBe(403); + expect(res.json().error).toBe('One or more links do not belong to your account'); + expect(mockPrisma.card.create).not.toHaveBeenCalled(); + }); + + it('creates the card when all linkIds are owned by the user', async () => { + mockPrisma.platformLink.findMany.mockResolvedValue([{ id: OWNED_LINK_ID }]); + mockPrisma.card.count.mockResolvedValue(0); + mockPrisma.card.create.mockResolvedValue({ ...mockCard, cardLinks: [] }); + + const app = await buildApp(); + const res = await app.inject({ + method: 'POST', + url: '/api/cards', + payload: { title: 'Test Card', linkIds: [OWNED_LINK_ID] }, + }); + + expect(res.statusCode).toBe(201); + expect(mockPrisma.platformLink.findMany).toHaveBeenCalledWith({ + where: { id: { in: [OWNED_LINK_ID] }, userId: USER_ID }, + select: { id: true }, + }); + }); + + it('skips the ownership check and creates the card when linkIds is empty', async () => { + mockPrisma.card.count.mockResolvedValue(1); + mockPrisma.card.create.mockResolvedValue({ ...mockCard, isDefault: false, cardLinks: [] }); + + const app = await buildApp(); + const res = await app.inject({ + method: 'POST', + url: '/api/cards', + payload: { title: 'Empty Card', linkIds: [] }, + }); + + expect(res.statusCode).toBe(201); + expect(mockPrisma.platformLink.findMany).not.toHaveBeenCalled(); + }); + + it('returns 500 when the ownership query throws unexpectedly', async () => { + mockPrisma.platformLink.findMany.mockRejectedValue(new Error('DB connection lost')); + + const app = await buildApp(); + const res = await app.inject({ + method: 'POST', + url: '/api/cards', + payload: { title: 'Test Card', linkIds: [OWNED_LINK_ID] }, + }); + + expect(res.statusCode).toBe(500); + // No write must have been attempted after the read failure + expect(mockPrisma.card.create).not.toHaveBeenCalled(); + }); + + it('returns 500 when card.count throws and no partial write occurs', async () => { + mockPrisma.platformLink.findMany.mockResolvedValue([{ id: OWNED_LINK_ID }]); + mockPrisma.card.count.mockRejectedValue(new Error('Query timeout')); + + const app = await buildApp(); + const res = await app.inject({ + method: 'POST', + url: '/api/cards', + payload: { title: 'Test Card', linkIds: [OWNED_LINK_ID] }, + }); + + expect(res.statusCode).toBe(500); + expect(mockPrisma.card.create).not.toHaveBeenCalled(); + }); + + it('returns 500 when card.create throws', async () => { + mockPrisma.platformLink.findMany.mockResolvedValue([{ id: OWNED_LINK_ID }]); + mockPrisma.card.count.mockResolvedValue(0); + mockPrisma.card.create.mockRejectedValue(new Error('FK constraint violation')); + + const app = await buildApp(); + const res = await app.inject({ + method: 'POST', + url: '/api/cards', + payload: { title: 'Test Card', linkIds: [OWNED_LINK_ID] }, + }); + + expect(res.statusCode).toBe(500); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// PUT /api/cards/:id +// ───────────────────────────────────────────────────────────────────────────── + +describe('PUT /api/cards/:id — link ownership validation', () => { + beforeEach(() => { + vi.clearAllMocks(); + wireTransaction(); + }); + + it('returns 403 when a supplied linkId belongs to another user', async () => { + mockPrisma.card.findFirst.mockResolvedValue(mockCard); + mockPrisma.platformLink.findMany.mockResolvedValue([]); + + const app = await buildApp(); + const res = await app.inject({ + method: 'PUT', + url: `/api/cards/${CARD_ID}`, + payload: { linkIds: [FOREIGN_LINK_ID] }, + }); + + expect(res.statusCode).toBe(403); + expect(res.json().error).toBe('One or more links do not belong to your account'); + // Existing links must not have been touched + expect(mockPrisma.$transaction).not.toHaveBeenCalled(); + expect(mockPrisma.cardLink.deleteMany).not.toHaveBeenCalled(); + expect(mockPrisma.cardLink.createMany).not.toHaveBeenCalled(); + }); + + it('updates links atomically when all supplied linkIds are owned', async () => { + mockPrisma.card.findFirst.mockResolvedValue(mockCard); + mockPrisma.platformLink.findMany.mockResolvedValue([{ id: OWNED_LINK_ID }]); + mockPrisma.cardLink.deleteMany.mockResolvedValue({ count: 0 }); + mockPrisma.cardLink.createMany.mockResolvedValue({ count: 1 }); + mockPrisma.card.findUnique.mockResolvedValue({ ...mockCard, cardLinks: [] }); + + const app = await buildApp(); + const res = await app.inject({ + method: 'PUT', + url: `/api/cards/${CARD_ID}`, + payload: { linkIds: [OWNED_LINK_ID] }, + }); + + expect(res.statusCode).toBe(200); + expect(mockPrisma.platformLink.findMany).toHaveBeenCalledWith({ + where: { id: { in: [OWNED_LINK_ID] }, userId: USER_ID }, + select: { id: true }, + }); + // Both operations must run inside the transaction, not as bare queries + expect(mockPrisma.$transaction).toHaveBeenCalledOnce(); + expect(mockPrisma.cardLink.deleteMany).toHaveBeenCalledWith({ where: { cardId: CARD_ID } }); + expect(mockPrisma.cardLink.createMany).toHaveBeenCalled(); + }); + + it('returns 404 when the card does not belong to the user', async () => { + mockPrisma.card.findFirst.mockResolvedValue(null); + + const app = await buildApp(); + const res = await app.inject({ + method: 'PUT', + url: `/api/cards/${CARD_ID}`, + payload: { linkIds: [OWNED_LINK_ID] }, + }); + + expect(res.statusCode).toBe(404); + expect(mockPrisma.platformLink.findMany).not.toHaveBeenCalled(); + }); + + it('returns 500 when the ownership query throws and no mutation occurs', async () => { + mockPrisma.card.findFirst.mockResolvedValue(mockCard); + mockPrisma.platformLink.findMany.mockRejectedValue(new Error('DB timeout')); + + const app = await buildApp(); + const res = await app.inject({ + method: 'PUT', + url: `/api/cards/${CARD_ID}`, + payload: { linkIds: [OWNED_LINK_ID] }, + }); + + expect(res.statusCode).toBe(500); + expect(mockPrisma.$transaction).not.toHaveBeenCalled(); + expect(mockPrisma.cardLink.deleteMany).not.toHaveBeenCalled(); + }); + + it('returns 500 and preserves existing links when the transaction fails mid-flight', async () => { + // Ownership check passes; deleteMany succeeds; createMany fails. + // The transaction rolls back, so the card retains its original links. + mockPrisma.card.findFirst.mockResolvedValue(mockCard); + mockPrisma.platformLink.findMany.mockResolvedValue([{ id: OWNED_LINK_ID }]); + mockPrisma.cardLink.deleteMany.mockResolvedValue({ count: 1 }); + mockPrisma.cardLink.createMany.mockRejectedValue(new Error('FK constraint')); + + const app = await buildApp(); + const res = await app.inject({ + method: 'PUT', + url: `/api/cards/${CARD_ID}`, + payload: { linkIds: [OWNED_LINK_ID] }, + }); + + expect(res.statusCode).toBe(500); + // Both were attempted inside the transaction (the DB rolls them back together) + expect(mockPrisma.cardLink.deleteMany).toHaveBeenCalled(); + expect(mockPrisma.cardLink.createMany).toHaveBeenCalled(); + // The final read must not have been called -- we short-circuited on error + expect(mockPrisma.card.findUnique).not.toHaveBeenCalled(); + }); + + it('returns 500 when card.findFirst throws', async () => { + mockPrisma.card.findFirst.mockRejectedValue(new Error('Connection refused')); + + const app = await buildApp(); + const res = await app.inject({ + method: 'PUT', + url: `/api/cards/${CARD_ID}`, + payload: { linkIds: [OWNED_LINK_ID] }, + }); + + expect(res.statusCode).toBe(500); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// DELETE /api/cards/:id +// ───────────────────────────────────────────────────────────────────────────── + +describe('DELETE /api/cards/:id', () => { + beforeEach(() => { + vi.clearAllMocks(); + wireTransaction(); + }); + + it('returns 204 on successful deletion of a non-default card', async () => { + mockPrisma.card.findFirst.mockResolvedValue({ ...mockCard, isDefault: false }); + mockPrisma.card.count.mockResolvedValue(2); + mockPrisma.card.delete.mockResolvedValue(mockCard); + + const app = await buildApp(); + const res = await app.inject({ method: 'DELETE', url: `/api/cards/${CARD_ID}` }); + + expect(res.statusCode).toBe(204); + expect(mockPrisma.card.delete).toHaveBeenCalledWith({ where: { id: CARD_ID } }); + // No reassignment needed for a non-default card + expect(mockPrisma.card.update).not.toHaveBeenCalled(); + }); + + it('returns 204 and reassigns default when deleting the current default card', async () => { + const otherCard = { id: 'card-other', isDefault: false, userId: USER_ID }; + // First findFirst: card being deleted. Second findFirst: oldest remaining. + mockPrisma.card.findFirst + .mockResolvedValueOnce({ ...mockCard, isDefault: true }) + .mockResolvedValueOnce(otherCard); + mockPrisma.card.count.mockResolvedValue(2); + mockPrisma.card.update.mockResolvedValue({ ...otherCard, isDefault: true }); + mockPrisma.card.delete.mockResolvedValue(mockCard); + + const app = await buildApp(); + const res = await app.inject({ method: 'DELETE', url: `/api/cards/${CARD_ID}` }); + + expect(res.statusCode).toBe(204); + expect(mockPrisma.card.update).toHaveBeenCalledWith({ + where: { id: otherCard.id }, + data: { isDefault: true }, + }); + expect(mockPrisma.card.delete).toHaveBeenCalledWith({ where: { id: CARD_ID } }); + }); + + it('returns 404 when the card is not owned by the user', async () => { + mockPrisma.card.findFirst.mockResolvedValue(null); + + const app = await buildApp(); + const res = await app.inject({ method: 'DELETE', url: `/api/cards/${CARD_ID}` }); + + expect(res.statusCode).toBe(404); + expect(mockPrisma.card.delete).not.toHaveBeenCalled(); + }); + + it('returns 400 when attempting to delete the last remaining card', async () => { + mockPrisma.card.findFirst.mockResolvedValue(mockCard); + mockPrisma.card.count.mockResolvedValue(1); + + const app = await buildApp(); + const res = await app.inject({ method: 'DELETE', url: `/api/cards/${CARD_ID}` }); + + expect(res.statusCode).toBe(400); + expect(res.json().error).toBe('Cannot delete the last remaining card. A user must have at least one card.'); + expect(mockPrisma.card.delete).not.toHaveBeenCalled(); + }); + + it('returns 500 when card.delete throws', async () => { + mockPrisma.card.findFirst.mockResolvedValue({ ...mockCard, isDefault: false }); + mockPrisma.card.count.mockResolvedValue(2); + mockPrisma.card.delete.mockRejectedValue(new Error('Deadlock detected')); + + const app = await buildApp(); + const res = await app.inject({ method: 'DELETE', url: `/api/cards/${CARD_ID}` }); + + expect(res.statusCode).toBe(500); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// PUT /api/cards/:id/default +// ───────────────────────────────────────────────────────────────────────────── + +describe('PUT /api/cards/:id/default', () => { + beforeEach(() => { + vi.clearAllMocks(); + wireTransaction(); + }); + + it('returns 200 and sets the card as default', async () => { + mockPrisma.card.findFirst.mockResolvedValue(mockCard); + mockPrisma.card.updateMany.mockResolvedValue({ count: 2 }); + mockPrisma.card.update.mockResolvedValue({ ...mockCard, isDefault: true }); + + const app = await buildApp(); + const res = await app.inject({ method: 'PUT', url: `/api/cards/${CARD_ID}/default` }); + + expect(res.statusCode).toBe(200); + expect(res.json().message).toBe('Default card updated'); + expect(mockPrisma.$transaction).toHaveBeenCalledOnce(); + // Clear-all and set-one must both run inside the transaction + expect(mockPrisma.card.updateMany).toHaveBeenCalledWith({ + where: { userId: USER_ID }, + data: { isDefault: false }, + }); + expect(mockPrisma.card.update).toHaveBeenCalledWith({ + where: { id: CARD_ID }, + data: { isDefault: true }, + }); + }); + + it('returns 404 when the card is not owned by the user', async () => { + mockPrisma.card.findFirst.mockResolvedValue(null); + + const app = await buildApp(); + const res = await app.inject({ method: 'PUT', url: `/api/cards/${CARD_ID}/default` }); + + expect(res.statusCode).toBe(404); + expect(mockPrisma.$transaction).not.toHaveBeenCalled(); + }); + + it('returns 500 and rolls back when the transaction fails mid-flight', async () => { + // updateMany clears all defaults; then update fails => transaction aborts, + // the user retains a consistent default card rather than having none. + mockPrisma.card.findFirst.mockResolvedValue(mockCard); + mockPrisma.card.updateMany.mockResolvedValue({ count: 2 }); + mockPrisma.card.update.mockRejectedValue(new Error('DB write failure')); + + const app = await buildApp(); + const res = await app.inject({ method: 'PUT', url: `/api/cards/${CARD_ID}/default` }); + + expect(res.statusCode).toBe(500); + expect(mockPrisma.card.updateMany).toHaveBeenCalled(); + expect(mockPrisma.card.update).toHaveBeenCalled(); + }); +}); diff --git a/apps/backend/src/__tests__/event.test.ts b/apps/backend/src/__tests__/event.test.ts new file mode 100644 index 00000000..44806af1 --- /dev/null +++ b/apps/backend/src/__tests__/event.test.ts @@ -0,0 +1,686 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import Fastify, { FastifyInstance } from 'fastify'; +import { PrismaClient } from '@prisma/client'; +import { eventRoutes } from '../routes/event'; + +// ─── Shared mock data ──────────────────────────────────────────────────────── + +const MOCK_USER_ID = 'user-uuid-001'; +const MOCK_OTHER_USER_ID = 'user-uuid-002'; + +const MOCK_EVENT = { + id: 'event-uuid-001', + name: 'DevCard Conf 2025', + slug: 'devcard-conf-2025', + description: 'Annual DevCard conference', + location: 'San Francisco, CA', + organizerId: MOCK_USER_ID, + startDate: new Date('2025-09-01T09:00:00Z'), + endDate: new Date('2025-09-02T18:00:00Z'), + isPublic: true, + createdAt: new Date('2025-01-01T00:00:00Z'), +}; + +const MOCK_USER_PROFILE = { + id: MOCK_USER_ID, + username: 'johndoe', + displayName: 'John Doe', + bio: 'Software engineer', + pronouns: 'he/him', + company: 'Acme Corp', + avatarUrl: 'https://example.com/avatar.png', + accentColor: '#6366f1', +}; + +const MOCK_OTHER_USER_PROFILE = { + id: MOCK_OTHER_USER_ID, + username: 'janedoe', + displayName: 'Jane Doe', + bio: null, + pronouns: null, + company: null, + avatarUrl: null, + accentColor: '#6366f1', +}; + +// ─── Prisma mock ───────────────────────────────────────────────────────────── + +const prismaMock = { + event: { + create: vi.fn(), + findUnique: vi.fn(), + }, + eventAttendee: { + create: vi.fn(), + delete: vi.fn(), + }, +}; + +// ─── App factory ───────────────────────────────────────────────────────────── +// +// Builds a minimal Fastify instance that wires up: +// • app.prisma – the Prisma mock above +// • request.jwtVerify() – overridden per-test via `mockJwtVerify` +// +// This mirrors the real app setup without touching a real DB or real JWT keys. + +let mockJwtVerify = vi.fn(); + +async function buildApp(): Promise { + const app = Fastify({ logger: false }); + + // Decorate prisma so routes can use app.prisma.* + app.decorate('prisma', prismaMock as unknown as PrismaClient); + + // Decorate jwtVerify on the request prototype so request.jwtVerify() resolves + // to whatever the current test wants. + app.decorateRequest('jwtVerify', function () { + return mockJwtVerify(); + }); + + // Register with the same prefix used in production (app.ts) so that + // tests exercise routes at their real paths — /api/events, /api/events/:slug, etc. + await app.register(eventRoutes, { prefix: '/api/events' }); + await app.ready(); + return app; +} + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +/** Returns a valid JWT-authenticated inject payload */ +function authHeader(): Record { + return { Authorization: 'Bearer mock-token' }; +} + +/** Injects a POST /api/events request */ +async function createEvent( + app: FastifyInstance, + body: Record, + authenticated = true, +) { + return app.inject({ + method: 'POST', + url: '/api/events', + headers: authenticated ? authHeader() : {}, + payload: body, + }); +} + +// ─── Test suite ────────────────────────────────────────────────────────────── + +describe('Events API', () => { + let app: FastifyInstance; + + beforeEach(async () => { + vi.clearAllMocks(); + // Default: authenticated as MOCK_USER_ID + mockJwtVerify.mockResolvedValue({ id: MOCK_USER_ID }); + app = await buildApp(); + }); + + afterEach(async () => { + await app.close(); + }); + + // ── POST /api/events ─────────────────────────────────────────────────────── + + describe('POST /api/events — create event', () => { + const validBody = { + name: 'DevCard Conf 2025', + description: 'Annual DevCard conference', + location: 'San Francisco, CA', + startDate: '2025-09-01T09:00:00Z', + endDate: '2025-09-02T18:00:00Z', + isPublic: true, + }; + + it('201 — creates event and returns it for authenticated organizer', async () => { + prismaMock.event.findUnique.mockResolvedValue(null); // slug is free + prismaMock.event.create.mockResolvedValue(MOCK_EVENT); + + const res = await createEvent(app, validBody); + + expect(res.statusCode).toBe(201); + const body = res.json(); + expect(body.slug).toBe('devcard-conf-2025'); + expect(body.organizerId).toBe(MOCK_USER_ID); + expect(body.location).toBe('San Francisco, CA'); + + // Prisma was called with correct fields + expect(prismaMock.event.create).toHaveBeenCalledOnce(); + const callArg = prismaMock.event.create.mock.calls[0][0].data; + expect(callArg.name).toBe('DevCard Conf 2025'); + expect(callArg.organizerId).toBe(MOCK_USER_ID); + expect(callArg.location).toBe('San Francisco, CA'); + }); + + it('401 — rejects unauthenticated request', async () => { + mockJwtVerify.mockRejectedValue(new Error('Unauthorized')); + + const res = await createEvent(app, validBody, false); + + expect(res.statusCode).toBe(401); + expect(res.json()).toMatchObject({ error: 'Unauthorized' }); + }); + + it('400 — rejects missing required fields (no dates, no location)', async () => { + const res = await createEvent(app, { name: 'Hello World' }); // missing dates + location + expect(res.statusCode).toBe(400); + }); + + it('400 — rejects missing location', async () => { + const { location: _omit, ...bodyWithoutLocation } = validBody; + const res = await createEvent(app, bodyWithoutLocation); + expect(res.statusCode).toBe(400); + }); + + it('400 — rejects location shorter than 2 characters', async () => { + const res = await createEvent(app, { ...validBody, location: 'A' }); + expect(res.statusCode).toBe(400); + }); + + it('400 — rejects location longer than 100 characters', async () => { + const res = await createEvent(app, { ...validBody, location: 'A'.repeat(101) }); + expect(res.statusCode).toBe(400); + }); + + it('400 — rejects event name shorter than 3 characters', async () => { + const res = await createEvent(app, { ...validBody, name: 'Hi' }); + expect(res.statusCode).toBe(400); + }); + + it('400 — rejects event name longer than 100 characters', async () => { + const longName = 'A'.repeat(101); + const res = await createEvent(app, { ...validBody, name: longName }); + expect(res.statusCode).toBe(400); + }); + + it('400 — rejects invalid date format', async () => { + const res = await createEvent(app, { + ...validBody, + startDate: 'not-a-date', + }); + expect(res.statusCode).toBe(400); + }); + + it('201 — generates a unique slug when the first candidate is taken', async () => { + // First findUnique returns a conflict, second returns null (slug free) + prismaMock.event.findUnique + .mockResolvedValueOnce(MOCK_EVENT) // slug taken + .mockResolvedValueOnce(null); // randomised slug free + + prismaMock.event.create.mockResolvedValue({ + ...MOCK_EVENT, + slug: 'devcard-conf-2025-ab12', + }); + + const res = await createEvent(app, validBody); + + expect(res.statusCode).toBe(201); + // create was eventually called with a slug different from the base one + const createdSlug: string = prismaMock.event.create.mock.calls[0][0].data.slug; + expect(createdSlug).toMatch(/^devcard-conf-2025-[a-z0-9]+$/); + }); + + it('201 — isPublic defaults to true when omitted', async () => { + prismaMock.event.findUnique.mockResolvedValue(null); + prismaMock.event.create.mockResolvedValue(MOCK_EVENT); + + const { isPublic: _omit, ...bodyWithoutIsPublic } = validBody; + const res = await createEvent(app, bodyWithoutIsPublic); + + expect(res.statusCode).toBe(201); + const callData = prismaMock.event.create.mock.calls[0][0].data; + expect(callData.isPublic).toBe(true); + }); + + it('500 — returns 500 when database write fails', async () => { + prismaMock.event.findUnique.mockResolvedValue(null); + prismaMock.event.create.mockRejectedValue(new Error('DB error')); + + const res = await createEvent(app, validBody); + + expect(res.statusCode).toBe(500); + expect(res.json()).toMatchObject({ error: 'Failed to create event' }); + }); + }); + + // ── GET /api/events/:slug ────────────────────────────────────────────────── + + describe('GET /api/events/:slug — event details', () => { + it('200 — returns event info with attendee count', async () => { + prismaMock.event.findUnique.mockResolvedValue({ + ...MOCK_EVENT, + _count: { attendees: 42 }, + }); + + const res = await app.inject({ + method: 'GET', + url: '/api/events/devcard-conf-2025', + }); + + expect(res.statusCode).toBe(200); + const body = res.json(); + expect(body.slug).toBe('devcard-conf-2025'); + expect(body.attendeesCount).toBe(42); + expect(body.location).toBe('San Francisco, CA'); + // organizerId is exposed (public info) + expect(body.organizerId).toBe(MOCK_USER_ID); + }); + + it('404 — returns 404 for unknown slug', async () => { + prismaMock.event.findUnique.mockResolvedValue(null); + + const res = await app.inject({ + method: 'GET', + url: '/api/events/ghost-event', + }); + + expect(res.statusCode).toBe(404); + expect(res.json()).toMatchObject({ error: 'Event not found' }); + }); + + it('200 — works without authentication (public endpoint)', async () => { + // Even if JWT would fail, this route should not call jwtVerify + mockJwtVerify.mockRejectedValue(new Error('Should not be called')); + prismaMock.event.findUnique.mockResolvedValue({ + ...MOCK_EVENT, + _count: { attendees: 0 }, + }); + + const res = await app.inject({ + method: 'GET', + url: '/api/events/devcard-conf-2025', + // No Authorization header + }); + + expect(res.statusCode).toBe(200); + expect(mockJwtVerify).not.toHaveBeenCalled(); + }); + }); + + // ── POST /api/events/:slug/join ──────────────────────────────────────────── + + describe('POST /api/events/:slug/join — join event', () => { + it('201 — authenticated user joins an existing event', async () => { + prismaMock.event.findUnique.mockResolvedValue(MOCK_EVENT); + prismaMock.eventAttendee.create.mockResolvedValue({ + id: 'attendee-uuid-001', + userId: MOCK_OTHER_USER_ID, + eventId: MOCK_EVENT.id, + joinedAt: new Date(), + }); + + mockJwtVerify.mockResolvedValue({ id: MOCK_OTHER_USER_ID }); + + const res = await app.inject({ + method: 'POST', + url: '/api/events/devcard-conf-2025/join', + headers: authHeader(), + }); + + expect(res.statusCode).toBe(201); + expect(res.json()).toMatchObject({ message: 'User joined successfully' }); + + const callData = prismaMock.eventAttendee.create.mock.calls[0][0].data; + expect(callData.eventId).toBe(MOCK_EVENT.id); + expect(callData.userId).toBe(MOCK_OTHER_USER_ID); + }); + + it('401 — rejects unauthenticated request', async () => { + mockJwtVerify.mockRejectedValue(new Error('Unauthorized')); + + const res = await app.inject({ + method: 'POST', + url: '/api/events/devcard-conf-2025/join', + }); + + expect(res.statusCode).toBe(401); + expect(res.json()).toMatchObject({ error: 'Unauthorized' }); + }); + + it('404 — returns 404 when event does not exist', async () => { + prismaMock.event.findUnique.mockResolvedValue(null); + + const res = await app.inject({ + method: 'POST', + url: '/api/events/ghost-event/join', + headers: authHeader(), + }); + + expect(res.statusCode).toBe(404); + expect(res.json()).toMatchObject({ error: 'Event not found' }); + }); + + it('409 — returns 409 when user already joined the event', async () => { + prismaMock.event.findUnique.mockResolvedValue(MOCK_EVENT); + // Prisma unique constraint error + const uniqueError = Object.assign(new Error('Unique constraint'), { + code: 'P2002', + }); + prismaMock.eventAttendee.create.mockRejectedValue(uniqueError); + + const res = await app.inject({ + method: 'POST', + url: '/api/events/devcard-conf-2025/join', + headers: authHeader(), + }); + + expect(res.statusCode).toBe(409); + expect(res.json()).toMatchObject({ error: 'Already joined' }); + }); + + it('500 — returns 500 on unexpected database error', async () => { + prismaMock.event.findUnique.mockResolvedValue(MOCK_EVENT); + prismaMock.eventAttendee.create.mockRejectedValue(new Error('DB error')); + + const res = await app.inject({ + method: 'POST', + url: '/api/events/devcard-conf-2025/join', + headers: authHeader(), + }); + + expect(res.statusCode).toBe(500); + expect(res.json()).toMatchObject({ error: 'Failed to join' }); + }); + }); + + // ── DELETE /api/events/:slug/leave ──────────────────────────────────────── + + describe('DELETE /api/events/:slug/leave — leave event', () => { + it('204 — authenticated user leaves an event they joined', async () => { + prismaMock.event.findUnique.mockResolvedValue(MOCK_EVENT); + prismaMock.eventAttendee.delete.mockResolvedValue({}); + + mockJwtVerify.mockResolvedValue({ id: MOCK_OTHER_USER_ID }); + + const res = await app.inject({ + method: 'DELETE', + url: '/api/events/devcard-conf-2025/leave', + headers: authHeader(), + }); + + expect(res.statusCode).toBe(204); + + // Verify the compound unique key used in the delete + const deleteArg = prismaMock.eventAttendee.delete.mock.calls[0][0].where; + expect(deleteArg).toMatchObject({ + userId_eventId: { + userId: MOCK_OTHER_USER_ID, + eventId: MOCK_EVENT.id, + }, + }); + }); + + it('401 — rejects unauthenticated request', async () => { + mockJwtVerify.mockRejectedValue(new Error('Unauthorized')); + + const res = await app.inject({ + method: 'DELETE', + url: '/api/events/devcard-conf-2025/leave', + }); + + expect(res.statusCode).toBe(401); + expect(res.json()).toMatchObject({ error: 'Unauthorized' }); + }); + + it('404 — returns 404 when event does not exist', async () => { + prismaMock.event.findUnique.mockResolvedValue(null); + + const res = await app.inject({ + method: 'DELETE', + url: '/api/events/ghost-event/leave', + headers: authHeader(), + }); + + expect(res.statusCode).toBe(404); + expect(res.json()).toMatchObject({ error: 'Event not found' }); + }); + + it('404 — returns 404 when user was never an attendee (P2025)', async () => { + prismaMock.event.findUnique.mockResolvedValue(MOCK_EVENT); + // Prisma record-not-found error + const notFoundError = Object.assign(new Error('Record not found'), { + code: 'P2025', + }); + prismaMock.eventAttendee.delete.mockRejectedValue(notFoundError); + + const res = await app.inject({ + method: 'DELETE', + url: '/api/events/devcard-conf-2025/leave', + headers: authHeader(), + }); + + expect(res.statusCode).toBe(404); + expect(res.json()).toMatchObject({ error: 'User not found' }); + }); + + it('500 — returns 500 on unexpected database error', async () => { + prismaMock.event.findUnique.mockResolvedValue(MOCK_EVENT); + prismaMock.eventAttendee.delete.mockRejectedValue(new Error('DB error')); + + const res = await app.inject({ + method: 'DELETE', + url: '/api/events/devcard-conf-2025/leave', + headers: authHeader(), + }); + + expect(res.statusCode).toBe(500); + expect(res.json()).toMatchObject({ error: 'Failed to leave' }); + }); + }); + + // ── GET /api/events/:slug/attendees ─────────────────────────────────────── + + describe('GET /api/events/:slug/attendees — paginated attendee list', () => { + /** Builds a raw EventAttendee row as Prisma returns it (with nested user) */ + function makeAttendeeRow( + profile: typeof MOCK_USER_PROFILE | typeof MOCK_OTHER_USER_PROFILE, + ) { + return { + id: `attendee-${profile.id}`, + userId: profile.id, + eventId: MOCK_EVENT.id, + joinedAt: new Date(), + user: { ...profile }, + }; + } + + it('200 — returns paginated attendees with default page/limit', async () => { + const attendeeRows = [ + makeAttendeeRow(MOCK_USER_PROFILE), + makeAttendeeRow(MOCK_OTHER_USER_PROFILE), + ]; + + prismaMock.event.findUnique.mockResolvedValue({ + ...MOCK_EVENT, + attendees: attendeeRows, + }); + + const res = await app.inject({ + method: 'GET', + url: '/api/events/devcard-conf-2025/attendees', + }); + + expect(res.statusCode).toBe(200); + const body = res.json(); + + expect(body.attendees).toHaveLength(2); + expect(body.attendees[0]).toMatchObject({ + id: MOCK_USER_ID, + username: 'johndoe', + displayName: 'John Doe', + }); + + expect(body.pagination).toMatchObject({ + page: 1, + limit: 10, + total: 2, + }); + }); + + it('200 — respects custom page and limit query params', async () => { + prismaMock.event.findUnique.mockResolvedValue({ + ...MOCK_EVENT, + attendees: [makeAttendeeRow(MOCK_OTHER_USER_PROFILE)], + }); + + const res = await app.inject({ + method: 'GET', + url: '/api/events/devcard-conf-2025/attendees?page=2&limit=5', + }); + + expect(res.statusCode).toBe(200); + const body = res.json(); + expect(body.pagination.page).toBe(2); + expect(body.pagination.limit).toBe(5); + + // Verify skip/take were passed correctly to Prisma + const includeArg = prismaMock.event.findUnique.mock.calls[0][0].include; + expect(includeArg.attendees.skip).toBe(5); // (page-1) * limit = 1 * 5 + expect(includeArg.attendees.take).toBe(5); + }); + + it('200 — caps limit at 50 even if higher value is requested', async () => { + prismaMock.event.findUnique.mockResolvedValue({ + ...MOCK_EVENT, + attendees: [], + }); + + const res = await app.inject({ + method: 'GET', + url: '/api/events/devcard-conf-2025/attendees?limit=200', + }); + + expect(res.statusCode).toBe(200); + const includeArg = prismaMock.event.findUnique.mock.calls[0][0].include; + expect(includeArg.attendees.take).toBe(50); + }); + + it('200 — treats page < 1 as page 1', async () => { + prismaMock.event.findUnique.mockResolvedValue({ + ...MOCK_EVENT, + attendees: [], + }); + + const res = await app.inject({ + method: 'GET', + url: '/api/events/devcard-conf-2025/attendees?page=0', + }); + + expect(res.statusCode).toBe(200); + const includeArg = prismaMock.event.findUnique.mock.calls[0][0].include; + expect(includeArg.attendees.skip).toBe(0); // page forced to 1 → skip = 0 + }); + + it('200 — returns empty attendees list for event with no attendees', async () => { + prismaMock.event.findUnique.mockResolvedValue({ + ...MOCK_EVENT, + attendees: [], + }); + + const res = await app.inject({ + method: 'GET', + url: '/api/events/devcard-conf-2025/attendees', + }); + + expect(res.statusCode).toBe(200); + const body = res.json(); + expect(body.attendees).toHaveLength(0); + expect(body.pagination.total).toBe(0); + }); + + it('200 — public profiles do not leak sensitive fields', async () => { + prismaMock.event.findUnique.mockResolvedValue({ + ...MOCK_EVENT, + attendees: [makeAttendeeRow(MOCK_USER_PROFILE)], + }); + + const res = await app.inject({ + method: 'GET', + url: '/api/events/devcard-conf-2025/attendees', + }); + + const attendee = res.json().attendees[0]; + + // These fields MUST be present + expect(attendee).toHaveProperty('id'); + expect(attendee).toHaveProperty('username'); + expect(attendee).toHaveProperty('displayName'); + expect(attendee).toHaveProperty('accentColor'); + + // These fields MUST NOT be present + expect(attendee).not.toHaveProperty('email'); + expect(attendee).not.toHaveProperty('provider'); + expect(attendee).not.toHaveProperty('providerId'); + expect(attendee).not.toHaveProperty('role'); + }); + + it('404 — returns 404 for unknown event slug', async () => { + prismaMock.event.findUnique.mockResolvedValue(null); + + const res = await app.inject({ + method: 'GET', + url: '/api/events/ghost-event/attendees', + }); + + expect(res.statusCode).toBe(404); + expect(res.json()).toMatchObject({ error: 'Event not found' }); + }); + + it('200 — attendees are ordered by joinedAt desc (latest first)', async () => { + prismaMock.event.findUnique.mockResolvedValue({ + ...MOCK_EVENT, + attendees: [], + }); + + await app.inject({ + method: 'GET', + url: '/api/events/devcard-conf-2025/attendees', + }); + + const includeArg = prismaMock.event.findUnique.mock.calls[0][0].include; + expect(includeArg.attendees.orderBy).toMatchObject({ joinedAt: 'desc' }); + }); + }); + + // ── Slug generation edge cases ──────────────────────────────────────────── + + describe('Slug generation', () => { + const baseBody = { + location: 'San Francisco, CA', + startDate: '2025-09-01T09:00:00Z', + endDate: '2025-09-02T18:00:00Z', + }; + + it('converts spaces and special characters to hyphens', async () => { + prismaMock.event.findUnique.mockResolvedValue(null); + prismaMock.event.create.mockResolvedValue({ ...MOCK_EVENT, slug: 'my-awesome-event' }); + + await createEvent(app, { ...baseBody, name: 'My Awesome Event!!!' }); + + const slug: string = prismaMock.event.create.mock.calls[0][0].data.slug; + expect(slug).toBe('my-awesome-event'); + }); + + it('strips leading and trailing hyphens from slug', async () => { + prismaMock.event.findUnique.mockResolvedValue(null); + prismaMock.event.create.mockResolvedValue({ ...MOCK_EVENT, slug: 'event-name' }); + + await createEvent(app, { ...baseBody, name: '---Event Name---' }); + + const slug: string = prismaMock.event.create.mock.calls[0][0].data.slug; + expect(slug).not.toMatch(/^-|-$/); + }); + + it('collapses multiple consecutive hyphens into one', async () => { + prismaMock.event.findUnique.mockResolvedValue(null); + prismaMock.event.create.mockResolvedValue({ ...MOCK_EVENT, slug: 'event-name' }); + + await createEvent(app, { ...baseBody, name: 'Event Name' }); + + const slug: string = prismaMock.event.create.mock.calls[0][0].data.slug; + expect(slug).not.toMatch(/--/); + }); + }); +}); \ No newline at end of file diff --git a/apps/backend/src/__tests__/follow.test.ts b/apps/backend/src/__tests__/follow.test.ts index 8338f606..41830018 100644 --- a/apps/backend/src/__tests__/follow.test.ts +++ b/apps/backend/src/__tests__/follow.test.ts @@ -1,5 +1,5 @@ -import Fastify from 'fastify'; -import { describe, expect, it, vi } from 'vitest'; +import Fastify, { FastifyInstance } from 'fastify'; +import { describe, expect, it, vi, beforeAll, beforeEach, afterAll } from 'vitest'; import { followRoutes } from '../routes/follow.js'; @@ -7,32 +7,57 @@ vi.mock('../utils/encryption.js', () => ({ decrypt: vi.fn(() => 'fake-access-token'), })); -describe('POST /api/follow/:platform/:targetUsername', () => { - it('returns 400 when API follow is not supported for the platform', async () => { - const app = Fastify({ logger: false }); +// ── Shared mock data ────────────────────────────────────────────────────────── - const findUnique = vi.fn().mockResolvedValue({ - id: 'token-1', - userId: 'user-1', - platform: 'unknown', - accessToken: 'encrypted-token', - }); +const MOCK_USER_ID = 'user-uuid-001'; - app.decorate('prisma', { - oAuthToken: { - findUnique, - }, - followLog: { - create: vi.fn(), - }, - }as any); +const MOCK_OAUTH_TOKEN = { + id: 'token-1', + userId: MOCK_USER_ID, + platform: 'unknown', + accessToken: 'encrypted-token', +}; - app.decorate('authenticate', async (request: any) => { - request.user = { id: 'user-1' }; - }); +// ── App factory ─────────────────────────────────────────────────────────────── + +function buildApp(overrides: { + oAuthToken?: Record; + followLog?: Record; +} = {}): FastifyInstance { + const app = Fastify({ logger: false }); - await app.register(followRoutes, { prefix: '/api/follow' }); - await app.ready(); + app.decorate('prisma', { + oAuthToken: { + findUnique: vi.fn(), + ...overrides.oAuthToken, + }, + followLog: { + create: vi.fn(), + deleteMany: vi.fn(), + ...overrides.followLog, + }, + } as any); + + app.decorate('authenticate', async (request: any) => { + request.user = { id: MOCK_USER_ID }; + }); + + return app; +} + +async function makeApp(overrides?: Parameters[0]): Promise { + const app = buildApp(overrides); + await app.register(followRoutes, { prefix: '/api/follow' }); + await app.ready(); + return app; +} + +// ───────────────────────────────────────────────────────────────────────────── + +describe('POST /api/follow/:platform/:targetUsername — API follow', () => { + it('returns 400 when API follow is not supported for the platform', async () => { + const findUnique = vi.fn().mockResolvedValue(MOCK_OAUTH_TOKEN); + const app = await makeApp({ oAuthToken: { findUnique } }); const response = await app.inject({ method: 'POST', @@ -46,7 +71,7 @@ describe('POST /api/follow/:platform/:targetUsername', () => { expect(findUnique).toHaveBeenCalledWith({ where: { userId_platform: { - userId: 'user-1', + userId: MOCK_USER_ID, platform: 'unknown', }, }, @@ -54,4 +79,250 @@ describe('POST /api/follow/:platform/:targetUsername', () => { await app.close(); }); -}); \ No newline at end of file + + it('returns webview strategy and url for webview-strategy platforms (e.g. linkedin)', async () => { + const app = await makeApp(); + + const response = await app.inject({ + method: 'POST', + url: '/api/follow/linkedin/testuser', + }); + + const body = response.json(); + + expect(response.statusCode).toBe(200); + expect(body.strategy).toBe('webview'); + expect(body.url).toContain('linkedin.com/in/testuser'); + + await app.close(); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── + +describe('POST /api/follow/:platform/:targetUsername/log — follow log validation', () => { + let app: FastifyInstance; + let createLog: ReturnType; + + // One app instance shared across all log tests; mock reset between each test. + beforeAll(async () => { + createLog = vi.fn(); + app = await makeApp({ followLog: { create: createLog } }); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(() => { + createLog.mockReset(); + createLog.mockResolvedValue({ id: 'log-uuid-001' }); + }); + + // ── Valid payloads ──────────────────────────────────────────────────────── + + it('200 — accepts status: success, layer: foreground', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/follow/linkedin/testuser/log', + payload: { status: 'success', layer: 'foreground' }, + }); + + expect(res.statusCode).toBe(200); + expect(res.json()).toMatchObject({ status: 'success', logId: 'log-uuid-001' }); + expect(createLog).toHaveBeenCalledOnce(); + expect(createLog.mock.calls[0][0].data.status).toBe('success'); + }); + + it('200 — accepts status: failed', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/follow/linkedin/testuser/log', + payload: { status: 'failed', layer: 'foreground' }, + }); + + expect(res.statusCode).toBe(200); + expect(createLog).toHaveBeenCalledOnce(); + expect(createLog.mock.calls[0][0].data.status).toBe('failed'); + }); + + it('200 — accepts status: pending, layer: background', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/follow/linkedin/testuser/log', + payload: { status: 'pending', layer: 'background' }, + }); + + expect(res.statusCode).toBe(200); + expect(createLog).toHaveBeenCalledOnce(); + expect(createLog.mock.calls[0][0].data.layer).toBe('background'); + }); + + // ── Invalid status values — analytics integrity ─────────────────────────── + + it('400 — rejects invalid status "error" (old unvalidated internal value)', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/follow/linkedin/testuser/log', + payload: { status: 'error', layer: 'foreground' }, + }); + + expect(res.statusCode).toBe(400); + expect(res.json()).toMatchObject({ error: 'Invalid follow log payload' }); + // DB must NOT be written — this is the analytics integrity guarantee + expect(createLog).not.toHaveBeenCalled(); + }); + + it('400 — rejects arbitrary status string injection', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/follow/linkedin/testuser/log', + payload: { status: '"; DROP TABLE follow_logs; --', layer: 'foreground' }, + }); + + expect(res.statusCode).toBe(400); + expect(createLog).not.toHaveBeenCalled(); + }); + + // ── Invalid layer values — analytics integrity ──────────────────────────── + + // 'webview' was the old unvalidated default — it is now explicitly rejected. + // Any existing caller sending layer: 'webview' must migrate to 'foreground' + // (in-app WebView session) or 'background' (passive deep-link strategy). + it('400 — rejects legacy layer "webview" (old unvalidated default)', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/follow/linkedin/testuser/log', + payload: { status: 'success', layer: 'webview' }, + }); + + expect(res.statusCode).toBe(400); + expect(res.json()).toMatchObject({ error: 'Invalid follow log payload' }); + expect(createLog).not.toHaveBeenCalled(); + }); + + it('400 — rejects invalid layer "api"', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/follow/linkedin/testuser/log', + payload: { status: 'success', layer: 'api' }, + }); + + expect(res.statusCode).toBe(400); + expect(createLog).not.toHaveBeenCalled(); + }); + + // ── Malformed / missing payloads ────────────────────────────────────────── + + it('400 — rejects missing status field', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/follow/linkedin/testuser/log', + payload: { layer: 'foreground' }, + }); + + expect(res.statusCode).toBe(400); + expect(createLog).not.toHaveBeenCalled(); + }); + + it('400 — rejects missing layer field', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/follow/linkedin/testuser/log', + payload: { status: 'success' }, + }); + + expect(res.statusCode).toBe(400); + expect(createLog).not.toHaveBeenCalled(); + }); + + it('400 — rejects empty body', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/follow/linkedin/testuser/log', + payload: {}, + }); + + expect(res.statusCode).toBe(400); + expect(createLog).not.toHaveBeenCalled(); + }); + + // ── Correct data persisted to DB ────────────────────────────────────────── + + it('persists exactly the validated platform, targetUsername, status, and layer', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/follow/twitter/janedoe/log', + payload: { status: 'pending', layer: 'background' }, + }); + + expect(res.statusCode).toBe(200); + expect(createLog).toHaveBeenCalledOnce(); + + const written = createLog.mock.calls[0][0].data; + expect(written).toMatchObject({ + followerId: MOCK_USER_ID, + targetUsername: 'janedoe', + platform: 'twitter', + status: 'pending', + layer: 'background', + }); + }); + + // ── Response does not leak validation internals ─────────────────────────── + + it('400 response only exposes { error } — no schema internals or stack traces', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/follow/linkedin/testuser/log', + payload: { status: 'bad', layer: 'bad' }, + }); + + expect(res.statusCode).toBe(400); + const body = res.json(); + expect(body).not.toHaveProperty('issues'); + expect(body).not.toHaveProperty('stack'); + expect(Object.keys(body)).toEqual(['error']); + }); + + // ── DB failure after valid payload ──────────────────────────────────────── + + it('500 — returns 500 when DB write fails after successful validation', async () => { + createLog.mockRejectedValueOnce(new Error('DB connection lost')); + + const res = await app.inject({ + method: 'POST', + url: '/api/follow/linkedin/testuser/log', + payload: { status: 'success', layer: 'foreground' }, + }); + + expect(res.statusCode).toBe(500); + expect(res.json()).toMatchObject({ error: 'Failed to log follow event' }); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── + +describe('DELETE /api/follow/:platform/:targetUsername/log — clear follow log', () => { + it('clears follow log entries for the authenticated user', async () => { + const deleteMany = vi.fn().mockResolvedValue({ count: 1 }); + const app = await makeApp({ followLog: { deleteMany } }); + + const response = await app.inject({ + method: 'DELETE', + url: '/api/follow/linkedin/testuser/log', + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toMatchObject({ status: 'cleared' }); + expect(deleteMany).toHaveBeenCalledWith({ + where: { + followerId: MOCK_USER_ID, + platform: 'linkedin', + targetUsername: 'testuser', + }, + }); + + await app.close(); + }); +}); diff --git a/apps/backend/src/__tests__/oauth-scope.test.ts b/apps/backend/src/__tests__/oauth-scope.test.ts new file mode 100644 index 00000000..0985dfa7 --- /dev/null +++ b/apps/backend/src/__tests__/oauth-scope.test.ts @@ -0,0 +1,392 @@ +/** + * Regression tests for OAuth token scope isolation. + * + * Prior to the fix, the authentication flow (scope: read:user user:email, + * platform key: 'github') and the GitHub connect/follow flow (scope: + * user:follow, platform key: 'github') both wrote to the same OAuthToken + * record. Whichever executed last silently overwrote the other's access + * token, causing follow capability to disappear after re-authentication. + * + * The fix uses a dedicated platform key ('github_follow') for the connect + * flow so the two records are independent and can never overwrite each other. + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import Fastify from 'fastify'; +import { connectRoutes } from '../routes/connect.js'; +import { followRoutes } from '../routes/follow.js'; +import type { PrismaClient } from '@prisma/client'; + +// ── Mocks ───────────────────────────────────────────────────────────────────── + +vi.mock('../utils/encryption.js', () => ({ + encrypt: vi.fn((v: string) => `enc:${v}`), + decrypt: vi.fn((v: string) => v.replace(/^enc:/, '')), +})); + +const mockFetch = vi.fn(); +beforeEach(() => { + vi.clearAllMocks(); + (globalThis as any).fetch = mockFetch; + + process.env.PUBLIC_APP_URL = 'http://localhost:5173'; + process.env.BACKEND_URL = 'http://localhost:3000'; + process.env.GITHUB_CLIENT_ID = 'test-client-id'; +}); + +const USER_ID = 'user-scope-test'; + +// ── Connect-route test harness ──────────────────────────────────────────────── + +function makeConnectState(userId: string): string { + return Buffer.from(JSON.stringify({ userId, nonce: 'test-nonce' })).toString('base64'); +} + +function buildConnectApp(mockPrisma: Partial) { + const app = Fastify({ logger: false }); + app.decorate('prisma', mockPrisma as PrismaClient); + app.decorate('authenticate', async (req: any) => { req.user = { id: USER_ID }; }); + app.register(connectRoutes, { prefix: '/api/connect' }); + return app.ready().then(() => app); +} + +// ── Follow-route test harness ───────────────────────────────────────────────── + +function buildFollowApp(mockPrisma: Partial) { + const app = Fastify({ logger: false }); + app.decorate('prisma', mockPrisma as PrismaClient); + app.decorate('authenticate', async (req: any) => { req.user = { id: USER_ID }; }); + app.register(followRoutes, { prefix: '/api/follow' }); + return app.ready().then(() => app); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 1. Connect flow — platform key +// ───────────────────────────────────────────────────────────────────────────── + +describe('GitHub connect flow — token stored under github_follow', () => { + it('writes the follow token to platform=github_follow, not github', async () => { + const upsert = vi.fn().mockResolvedValue({}); + const app = await buildConnectApp({ oAuthToken: { upsert } as any }); + + mockFetch.mockResolvedValue({ + json: async () => ({ access_token: 'follow-token', scope: 'user:follow' }), + }); + + await app.inject({ + method: 'GET', + url: `/api/connect/github/callback?code=code123&state=${makeConnectState(USER_ID)}`, + }); + + expect(upsert).toHaveBeenCalledOnce(); + const call = upsert.mock.calls[0][0]; + + // Key must be github_follow + expect(call.where.userId_platform.platform).toBe('github_follow'); + expect(call.create.platform).toBe('github_follow'); + expect(call.update).not.toHaveProperty('platform'); // update never changes the key + }); + + it('stores the scope returned by GitHub in the follow token record', async () => { + const upsert = vi.fn().mockResolvedValue({}); + const app = await buildConnectApp({ oAuthToken: { upsert } as any }); + + mockFetch.mockResolvedValue({ + json: async () => ({ access_token: 'follow-token', scope: 'user:follow' }), + }); + + await app.inject({ + method: 'GET', + url: `/api/connect/github/callback?code=code123&state=${makeConnectState(USER_ID)}`, + }); + + const { create } = upsert.mock.calls[0][0]; + expect(create.scopes).toBe('user:follow'); + }); + + it('falls back to user:follow scope when GitHub omits the scope field', async () => { + const upsert = vi.fn().mockResolvedValue({}); + const app = await buildConnectApp({ oAuthToken: { upsert } as any }); + + mockFetch.mockResolvedValue({ + json: async () => ({ access_token: 'follow-token' }), // no scope field + }); + + await app.inject({ + method: 'GET', + url: `/api/connect/github/callback?code=code123&state=${makeConnectState(USER_ID)}`, + }); + + const { create } = upsert.mock.calls[0][0]; + expect(create.scopes).toBe('user:follow'); + }); + + it('does NOT touch the github (auth) token record during the connect flow', async () => { + const upsert = vi.fn().mockResolvedValue({}); + const app = await buildConnectApp({ oAuthToken: { upsert } as any }); + + mockFetch.mockResolvedValue({ + json: async () => ({ access_token: 'follow-token', scope: 'user:follow' }), + }); + + await app.inject({ + method: 'GET', + url: `/api/connect/github/callback?code=code123&state=${makeConnectState(USER_ID)}`, + }); + + // Exactly one upsert — the github_follow record; never 'github' + expect(upsert).toHaveBeenCalledTimes(1); + const key = upsert.mock.calls[0][0].where.userId_platform.platform; + expect(key).not.toBe('github'); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// 2. Follow route — uses github_follow token +// ───────────────────────────────────────────────────────────────────────────── + +describe('GitHub follow route — looks up github_follow token', () => { + it('resolves the token from platform=github_follow', async () => { + const findUnique = vi.fn().mockResolvedValue({ + id: 'tok-1', + accessToken: 'enc:follow-token', + }); + + mockFetch.mockResolvedValue({ status: 204 }); + + const app = await buildFollowApp({ + oAuthToken: { findUnique } as any, + followLog: { create: vi.fn().mockReturnValue({ catch: vi.fn() }) } as any, + }); + + const res = await app.inject({ + method: 'POST', + url: '/api/follow/github/targetuser', + }); + + expect(res.statusCode).toBe(200); + expect(findUnique).toHaveBeenCalledWith({ + where: { userId_platform: { userId: USER_ID, platform: 'github_follow' } }, + }); + }); + + it('returns 400 with requiresAuth when github_follow token is absent', async () => { + const findUnique = vi.fn().mockResolvedValue(null); + + const app = await buildFollowApp({ + oAuthToken: { findUnique } as any, + followLog: { create: vi.fn() } as any, + }); + + const res = await app.inject({ + method: 'POST', + url: '/api/follow/github/targetuser', + }); + + expect(res.statusCode).toBe(400); + expect(res.json().requiresAuth).toBe(true); + + // The lookup must be for github_follow — not the auth token + expect(findUnique.mock.calls[0][0].where.userId_platform.platform).toBe('github_follow'); + }); + + it('does NOT fall back to the github (auth) token if github_follow is missing', async () => { + // Only the github_follow lookup should be attempted; never a fallback to 'github' + const findUnique = vi.fn().mockResolvedValue(null); + + const app = await buildFollowApp({ + oAuthToken: { findUnique } as any, + followLog: { create: vi.fn() } as any, + }); + + await app.inject({ method: 'POST', url: '/api/follow/github/targetuser' }); + + // Exactly one DB call, and it is for github_follow + expect(findUnique).toHaveBeenCalledTimes(1); + expect(findUnique.mock.calls[0][0].where.userId_platform.platform).toBe('github_follow'); + }); + + it('non-GitHub platforms still use their own name as the token key', async () => { + const findUnique = vi.fn().mockResolvedValue({ + id: 'tok-2', + accessToken: 'enc:some-token', + }); + + const app = await buildFollowApp({ + oAuthToken: { findUnique } as any, + followLog: { create: vi.fn() } as any, + }); + + // 'twitter' is not GitHub — it should not be mapped to 'twitter_follow' + await app.inject({ method: 'POST', url: '/api/follow/twitter/targetuser' }); + + expect(findUnique.mock.calls[0][0].where.userId_platform.platform).toBe('twitter'); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// 3. Scope-overwrite lifecycle — the full story +// ───────────────────────────────────────────────────────────────────────────── + +describe('Scope-overwrite lifecycle — isolation between auth and connect tokens', () => { + it('connect flow and auth flow target independent platform keys (no shared record)', async () => { + // Simulate: user logs in → connect → log in again. + // The auth upsert writes 'github'; the connect upsert writes 'github_follow'. + // They never share a key, so neither can overwrite the other. + + const upsertCalls: string[] = []; + const upsert = vi.fn().mockImplementation(async (args: any) => { + upsertCalls.push(args.where.userId_platform.platform); + return {}; + }); + + // ── Simulate the connect callback ────────────────────────────────────── + const connectApp = await buildConnectApp({ oAuthToken: { upsert } as any }); + + mockFetch.mockResolvedValue({ + json: async () => ({ access_token: 'follow-token', scope: 'user:follow' }), + }); + + await connectApp.inject({ + method: 'GET', + url: `/api/connect/github/callback?code=code1&state=${makeConnectState(USER_ID)}`, + }); + + // Connect writes github_follow + expect(upsertCalls).toContain('github_follow'); + expect(upsertCalls).not.toContain('github'); + }); + + it('repeated connect cycles only ever touch github_follow', async () => { + const upsert = vi.fn().mockResolvedValue({}); + const app = await buildConnectApp({ oAuthToken: { upsert } as any }); + + mockFetch.mockResolvedValue({ + json: async () => ({ access_token: 'follow-token', scope: 'user:follow' }), + }); + + // Three consecutive reconnect attempts + for (let i = 0; i < 3; i++) { + await app.inject({ + method: 'GET', + url: `/api/connect/github/callback?code=code${i}&state=${makeConnectState(USER_ID)}`, + }); + } + + expect(upsert).toHaveBeenCalledTimes(3); + for (const call of upsert.mock.calls) { + expect(call[0].where.userId_platform.platform).toBe('github_follow'); + } + }); + + it('follow route succeeds immediately after a connect cycle', async () => { + const ENCRYPTED_FOLLOW_TOKEN = 'enc:follow-token-v1'; + + const findUnique = vi.fn().mockResolvedValue({ + id: 'tok-follow', + accessToken: ENCRYPTED_FOLLOW_TOKEN, + }); + + mockFetch.mockResolvedValue({ status: 204 }); // GitHub follow API + + const app = await buildFollowApp({ + oAuthToken: { findUnique } as any, + followLog: { create: vi.fn().mockReturnValue({ catch: vi.fn() }) } as any, + }); + + const res = await app.inject({ + method: 'POST', + url: '/api/follow/github/somedev', + }); + + expect(res.statusCode).toBe(200); + // Confirm we went to the GitHub API with the decrypted token + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining('api.github.com/user/following/somedev'), + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer follow-token-v1', + }), + }), + ); + }); + + it('follow route still works after a simulated re-login cycle', async () => { + // Re-login would call auth.ts → upsert to 'github'. + // The github_follow record is untouched. Follow should still resolve. + + const FOLLOW_TOKEN = { id: 'tok-follow', accessToken: 'enc:follow-token' }; + const findUnique = vi.fn().mockResolvedValue(FOLLOW_TOKEN); + + mockFetch.mockResolvedValue({ status: 204 }); + + const app = await buildFollowApp({ + oAuthToken: { findUnique } as any, + followLog: { create: vi.fn().mockReturnValue({ catch: vi.fn() }) } as any, + }); + + const res = await app.inject({ + method: 'POST', + url: '/api/follow/github/postlogindev', + }); + + expect(res.statusCode).toBe(200); + // Lookup must target github_follow, not the re-written auth token + expect(findUnique.mock.calls[0][0].where.userId_platform.platform).toBe('github_follow'); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// 4. Encrypted token persistence +// ───────────────────────────────────────────────────────────────────────────── + +describe('Encrypted token persistence', () => { + it('connect flow stores an encrypted token, not the raw access token', async () => { + const RAW_TOKEN = 'ghs_raw_follow_token_abc123'; + const upsert = vi.fn().mockResolvedValue({}); + + const app = await buildConnectApp({ oAuthToken: { upsert } as any }); + + mockFetch.mockResolvedValue({ + json: async () => ({ access_token: RAW_TOKEN, scope: 'user:follow' }), + }); + + await app.inject({ + method: 'GET', + url: `/api/connect/github/callback?code=code&state=${makeConnectState(USER_ID)}`, + }); + + const { create } = upsert.mock.calls[0][0]; + // encrypt mock prefixes with 'enc:' — raw token must not appear verbatim + expect(create.accessToken).toBe(`enc:${RAW_TOKEN}`); + expect(create.accessToken).not.toBe(RAW_TOKEN); + }); + + it('follow route decrypts the stored token before calling GitHub API', async () => { + const STORED = 'enc:decrypted-follow-token'; + + const findUnique = vi.fn().mockResolvedValue({ + id: 'tok-1', + accessToken: STORED, + }); + + mockFetch.mockResolvedValue({ status: 204 }); + + const app = await buildFollowApp({ + oAuthToken: { findUnique } as any, + followLog: { create: vi.fn().mockReturnValue({ catch: vi.fn() }) } as any, + }); + + await app.inject({ method: 'POST', url: '/api/follow/github/dev' }); + + // decrypt mock strips 'enc:' prefix + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer decrypted-follow-token', + }), + }), + ); + }); +}); diff --git a/apps/backend/src/__tests__/profiles.test.ts b/apps/backend/src/__tests__/profiles.test.ts index ef1aad65..07d10f98 100644 --- a/apps/backend/src/__tests__/profiles.test.ts +++ b/apps/backend/src/__tests__/profiles.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import Fastify from 'fastify'; import { profileRoutes } from '../routes/profiles.js'; +import type { PrismaClient } from '@prisma/client'; const mockUser = { id: 'user-123', @@ -19,17 +20,17 @@ const mockUser = { providerId: 'gh-123', }; -const mockPrisma = { +const mockPrisma: Pick = { user: { findUnique: vi.fn(), findFirst: vi.fn(), update: vi.fn(), - }, + } as unknown as PrismaClient['user'], }; async function buildApp() { const app = Fastify(); - app.decorate('prisma', mockPrisma); + app.decorate('prisma', mockPrisma as unknown as PrismaClient); app.decorate('authenticate', async (request: any) => { request.user = { id: 'user-123' }; }); @@ -89,7 +90,7 @@ describe('PUT /api/profiles/me', () => { expect(res.json().error).toBe('Validation failed'); }); - it('should return 409 if username is already taken', async () => { + it('should return 409 if username is already taken (pre-check)', async () => { mockPrisma.user.findFirst.mockResolvedValue({ id: 'other-user' }); const app = await buildApp(); const res = await app.inject({ @@ -100,4 +101,50 @@ describe('PUT /api/profiles/me', () => { expect(res.statusCode).toBe(409); expect(res.json().error).toBe('Username already taken'); }); + + it('should return 409 when a concurrent request wins the unique constraint race (P2002)', async () => { + // Both requests pass the findFirst check; the DB unique constraint fires on + // the losing write — Prisma raises P2002. + mockPrisma.user.findFirst.mockResolvedValue(null); + const p2002 = Object.assign(new Error('Unique constraint failed'), { code: 'P2002' }); + mockPrisma.user.update.mockRejectedValue(p2002); + + const app = await buildApp(); + const res = await app.inject({ + method: 'PUT', + url: '/api/profiles/me', + payload: { username: 'raced-username' }, + }); + + expect(res.statusCode).toBe(409); + expect(res.json().error).toBe('Username already taken'); + }); + + it('should return 500 for unexpected database errors during update', async () => { + mockPrisma.user.findFirst.mockResolvedValue(null); + mockPrisma.user.update.mockRejectedValue(new Error('Connection refused')); + + const app = await buildApp(); + const res = await app.inject({ + method: 'PUT', + url: '/api/profiles/me', + payload: { username: 'anyuser' }, + }); + + expect(res.statusCode).toBe(500); + expect(res.json().error).toBe('Internal server error'); + }); + + it('should not call findFirst when no username is provided in the update', async () => { + mockPrisma.user.update.mockResolvedValue({ ...mockUser, displayName: 'New Name' }); + const app = await buildApp(); + const res = await app.inject({ + method: 'PUT', + url: '/api/profiles/me', + payload: { displayName: 'New Name' }, + }); + + expect(res.statusCode).toBe(200); + expect(mockPrisma.user.findFirst).not.toHaveBeenCalled(); + }); }); \ No newline at end of file diff --git a/apps/backend/src/__tests__/public.test.ts b/apps/backend/src/__tests__/public.test.ts new file mode 100644 index 00000000..a767b25d --- /dev/null +++ b/apps/backend/src/__tests__/public.test.ts @@ -0,0 +1,466 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import Fastify from 'fastify'; +import jwt from '@fastify/jwt'; +import { publicRoutes } from '../routes/public.js'; +import type { PrismaClient } from '@prisma/client'; + +// ── Mock QR utilities ───────────────────────────────────────────────────────── +// Prevents real QR rasterisation (and any native canvas/image deps) from running +// during unit tests. The stubs return minimal valid values that satisfy the +// Content-Type assertions below. +vi.mock('../utils/qr.js', () => ({ + generateQRBuffer: vi.fn().mockResolvedValue(Buffer.from('fake-png')), + generateQRSvg: vi.fn().mockResolvedValue('fake'), +})); + +import { generateQRBuffer, generateQRSvg } from '../utils/qr.js'; + +const mockUser = { + id: 'user-123', + username: 'testuser', + displayName: 'Test User', + bio: null, + pronouns: null, + role: null, + company: null, + avatarUrl: null, + accentColor: '#ffffff', + platformLinks: [], +}; + +const mockPrisma = { + user: { + findUnique: vi.fn(), + }, + platformLink: {} as any, + cardView: { + create: vi.fn().mockReturnValue({ catch: vi.fn() }), + }, + followLog: { + findMany: vi.fn().mockResolvedValue([]), + }, + card: {} as any, +}; + +// ── Redis mock ──────────────────────────────────────────────────────────────── +// Simulates ioredis behaviour: get returns null (MISS) by default. +const mockRedis = { + get: vi.fn().mockResolvedValue(null), + set: vi.fn().mockResolvedValue('OK'), + del: vi.fn().mockResolvedValue(1), +}; + +async function buildApp() { + const app = Fastify(); + // Register JWT so app.jwt.sign() is available for the qr-session route. + // @fastify/jwt also adds request.jwtVerify(), which throws when no valid + // Authorization header is present — matching the soft-auth pattern in the routes. + await app.register(jwt, { secret: 'test-secret-for-unit-tests-only' }); + app.decorate('prisma', mockPrisma as unknown as PrismaClient); + // Decorate with the Redis mock so cache branches execute in tests. + app.decorate('redis', mockRedis as any); + app.register(publicRoutes, { prefix: '/api/public' }); + await app.ready(); + return app; +} + +// ─── QR size validation ─────────────────────────────────────────────────────── + +describe('GET /api/public/:username/qr — size validation', () => { + beforeEach(() => { + vi.clearAllMocks(); + // Re-attach default mock behaviour cleared by clearAllMocks + (generateQRBuffer as ReturnType).mockResolvedValue(Buffer.from('fake-png')); + (generateQRSvg as ReturnType).mockResolvedValue('fake'); + mockRedis.get.mockResolvedValue(null); + mockRedis.set.mockResolvedValue('OK'); + }); + + // ── Reject before DB touch ───────────────────────────────────────────────── + + it('rejects size=0 with 400 before any DB query', async () => { + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/testuser/qr?size=0', + }); + expect(res.statusCode).toBe(400); + expect(res.json().error).toMatch(/integer between/i); + expect(mockPrisma.user.findUnique).not.toHaveBeenCalled(); + }); + + it('rejects size=-1 with 400 before any DB query', async () => { + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/testuser/qr?size=-1', + }); + expect(res.statusCode).toBe(400); + expect(mockPrisma.user.findUnique).not.toHaveBeenCalled(); + }); + + it('rejects size=50000 (above upper bound) with 400', async () => { + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/testuser/qr?size=50000', + }); + expect(res.statusCode).toBe(400); + expect(res.json().error).toMatch(/integer between/i); + expect(mockPrisma.user.findUnique).not.toHaveBeenCalled(); + }); + + it('rejects size=2049 (one above upper bound) with 400', async () => { + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/testuser/qr?size=2049', + }); + expect(res.statusCode).toBe(400); + expect(mockPrisma.user.findUnique).not.toHaveBeenCalled(); + }); + + it('rejects non-numeric size (abc) with 400', async () => { + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/testuser/qr?size=abc', + }); + expect(res.statusCode).toBe(400); + expect(mockPrisma.user.findUnique).not.toHaveBeenCalled(); + }); + + it('rejects floating-point size (400.5) with 400', async () => { + // parseInt('400.5') === 400, which IS in range — this passes. + // Documenting the boundary: fractional strings are truncated, not rejected. + // A string like '0.5' parseInt → 0, which is out of range. + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/testuser/qr?size=0.5', + }); + expect(res.statusCode).toBe(400); + expect(mockPrisma.user.findUnique).not.toHaveBeenCalled(); + }); + + // ── Accept valid sizes ───────────────────────────────────────────────────── + + it('accepts size=1 (lower bound) and returns PNG', async () => { + mockPrisma.user.findUnique.mockResolvedValue(mockUser); + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/testuser/qr?size=1', + }); + expect(res.statusCode).toBe(200); + expect(res.headers['content-type']).toMatch(/image\/png/); + expect(generateQRBuffer).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ width: 1 }), + ); + }); + + it('accepts size=2048 (upper bound) and returns PNG', async () => { + mockPrisma.user.findUnique.mockResolvedValue(mockUser); + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/testuser/qr?size=2048', + }); + expect(res.statusCode).toBe(200); + expect(res.headers['content-type']).toMatch(/image\/png/); + expect(generateQRBuffer).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ width: 2048 }), + ); + }); + + it('defaults to size=400 when no size param is provided', async () => { + mockPrisma.user.findUnique.mockResolvedValue(mockUser); + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/testuser/qr', + }); + expect(res.statusCode).toBe(200); + expect(generateQRBuffer).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ width: 400 }), + ); + }); + + // ── Format selection ─────────────────────────────────────────────────────── + + it('returns SVG when format=svg is requested', async () => { + mockPrisma.user.findUnique.mockResolvedValue(mockUser); + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/testuser/qr?format=svg&size=200', + }); + expect(res.statusCode).toBe(200); + expect(res.headers['content-type']).toMatch(/image\/svg\+xml/); + expect(generateQRSvg).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ width: 200 }), + ); + }); + + // ── User not found ───────────────────────────────────────────────────────── + + it('returns 404 for an unknown username (valid size)', async () => { + mockPrisma.user.findUnique.mockResolvedValue(null); + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/nobody/qr?size=400', + }); + expect(res.statusCode).toBe(404); + expect(res.json().error).toBe('User not found'); + }); + + // ── QR generation error ──────────────────────────────────────────────────── + + it('returns 500 when QR generation throws', async () => { + mockPrisma.user.findUnique.mockResolvedValue(mockUser); + (generateQRBuffer as ReturnType).mockRejectedValueOnce( + new Error('canvas error'), + ); + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/testuser/qr?size=400', + }); + expect(res.statusCode).toBe(500); + expect(res.json().error).toBe('QR code generation failed'); + }); +}); + +// ─── Redis cache HIT / MISS behaviour ──────────────────────────────────────── + +describe('GET /api/public/:username — Redis cache', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockRedis.get.mockResolvedValue(null); + mockRedis.set.mockResolvedValue('OK'); + mockPrisma.followLog.findMany.mockResolvedValue([]); + mockPrisma.cardView.create.mockReturnValue({ catch: vi.fn() }); + }); + + it('returns X-Cache: MISS and queries DB on first request', async () => { + mockPrisma.user.findUnique.mockResolvedValue(mockUser); + const app = await buildApp(); + + const res = await app.inject({ + method: 'GET', + url: '/api/public/testuser', + }); + + expect(res.statusCode).toBe(200); + expect(res.headers['x-cache']).toBe('MISS'); + expect(res.headers['cache-control']).toBe('public, max-age=300, stale-while-revalidate=60'); + // DB was queried since Redis returned null + expect(mockPrisma.user.findUnique).toHaveBeenCalledOnce(); + // Profile should be written to Redis after the DB fetch + expect(mockRedis.set).toHaveBeenCalledWith( + 'profile:testuser', + expect.any(String), + 'EX', + 300, + ); + }); + + it('returns X-Cache: HIT and skips DB on cached request', async () => { + // Simulate a warm cache entry + const cached = JSON.stringify({ + _userId: 'user-123', + username: 'testuser', + displayName: 'Test User', + bio: null, + pronouns: null, + role: null, + company: null, + avatarUrl: null, + accentColor: '#ffffff', + links: [], + }); + mockRedis.get.mockResolvedValue(cached); + + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/testuser', + }); + + expect(res.statusCode).toBe(200); + expect(res.headers['x-cache']).toBe('HIT'); + // DB must NOT be queried when cache is warm + expect(mockPrisma.user.findUnique).not.toHaveBeenCalled(); + }); + + it('response body on cache HIT matches the cached profile', async () => { + const cached = JSON.stringify({ + _userId: 'user-123', + username: 'testuser', + displayName: 'Test User', + bio: 'A bio', + pronouns: null, + role: 'Engineer', + company: null, + avatarUrl: null, + accentColor: '#123456', + links: [], + }); + mockRedis.get.mockResolvedValue(cached); + + const app = await buildApp(); + const res = await app.inject({ method: 'GET', url: '/api/public/testuser' }); + const body = res.json(); + + expect(body.username).toBe('testuser'); + expect(body.accentColor).toBe('#123456'); + // Internal _userId field must not leak into the HTTP response + expect(body._userId).toBeUndefined(); + }); + + it('falls through to DB when Redis.get throws', async () => { + mockRedis.get.mockRejectedValue(new Error('Redis down')); + mockPrisma.user.findUnique.mockResolvedValue(mockUser); + + const app = await buildApp(); + const res = await app.inject({ method: 'GET', url: '/api/public/testuser' }); + + expect(res.statusCode).toBe(200); + // DB was reached despite the Redis failure + expect(mockPrisma.user.findUnique).toHaveBeenCalledOnce(); + }); + + it('returns 404 when user does not exist (cache MISS)', async () => { + mockPrisma.user.findUnique.mockResolvedValue(null); + + const app = await buildApp(); + const res = await app.inject({ method: 'GET', url: '/api/public/nobody' }); + + expect(res.statusCode).toBe(404); + expect(res.json().error).toBe('User not found'); + }); +}); + +// ─── QR session endpoint ────────────────────────────────────────────────────── + +describe('GET /api/public/:username/qr-session', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockRedis.get.mockResolvedValue(null); + mockRedis.set.mockResolvedValue('OK'); + }); + + it('returns 404 when the user does not exist', async () => { + mockPrisma.user.findUnique.mockResolvedValue(null); + + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/nobody/qr-session', + }); + + expect(res.statusCode).toBe(404); + expect(res.json().error).toBe('User not found'); + }); + + it('returns a JWT token with correct shape on DB fetch (cache MISS)', async () => { + mockPrisma.user.findUnique.mockResolvedValue(mockUser); + + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/testuser/qr-session', + }); + + expect(res.statusCode).toBe(200); + const body = res.json(); + expect(typeof body.token).toBe('string'); + expect(body.tokenType).toBe('JWT'); + expect(body.expiresIn).toBe(600); + expect(typeof body.expiresAt).toBe('string'); + // expiresAt must be a valid ISO 8601 date string + expect(new Date(body.expiresAt).getTime()).toBeGreaterThan(Date.now()); + }); + + it('token payload encodes the public profile snapshot', async () => { + mockPrisma.user.findUnique.mockResolvedValue(mockUser); + + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/testuser/qr-session', + }); + + const { token } = res.json(); + // Decode without verifying so we can inspect the payload in the test + const decoded = JSON.parse( + Buffer.from(token.split('.')[1], 'base64url').toString(), + ); + expect(decoded.sub).toBe('testuser'); + expect(decoded.profile.username).toBe('testuser'); + expect(decoded.profile.displayName).toBe('Test User'); + }); + + it('serves snapshot from Redis cache without querying DB', async () => { + const cached = JSON.stringify({ + _userId: 'user-123', + username: 'testuser', + displayName: 'Cached User', + bio: null, + pronouns: null, + role: null, + company: null, + avatarUrl: null, + accentColor: '#ffffff', + links: [], + }); + mockRedis.get.mockResolvedValue(cached); + + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/testuser/qr-session', + }); + + expect(res.statusCode).toBe(200); + // DB must not be reached when the cache is warm + expect(mockPrisma.user.findUnique).not.toHaveBeenCalled(); + + const { token } = res.json(); + const decoded = JSON.parse( + Buffer.from(token.split('.')[1], 'base64url').toString(), + ); + expect(decoded.profile.displayName).toBe('Cached User'); + }); + + it('includes Cache-Control header in qr-session response', async () => { + mockPrisma.user.findUnique.mockResolvedValue(mockUser); + + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/public/testuser/qr-session', + }); + + expect(res.headers['cache-control']).toBe('public, max-age=300, stale-while-revalidate=60'); + }); + + it('caches the profile in Redis when served from DB', async () => { + mockPrisma.user.findUnique.mockResolvedValue(mockUser); + + const app = await buildApp(); + await app.inject({ method: 'GET', url: '/api/public/testuser/qr-session' }); + + expect(mockRedis.set).toHaveBeenCalledWith( + 'profile:testuser', + expect.any(String), + 'EX', + 300, + ); + }); +}); diff --git a/apps/backend/src/__tests__/team.test.ts b/apps/backend/src/__tests__/team.test.ts new file mode 100644 index 00000000..350298a1 --- /dev/null +++ b/apps/backend/src/__tests__/team.test.ts @@ -0,0 +1,776 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import Fastify, { FastifyInstance } from 'fastify'; +import { PrismaClient, TeamRole } from '@prisma/client'; +import { teamRoutes } from '../routes/team'; + +// ─── Shared mock data ───────────────────────────────────────────────────────── + +const MOCK_OWNER_ID = 'user-uuid-001'; +const MOCK_MEMBER_ID = 'user-uuid-002'; +const MOCK_OUTSIDER_ID = 'user-uuid-003'; + +const MOCK_OWNER = { + id: MOCK_OWNER_ID, + username: 'johndoe', + displayName: 'John Doe', + bio: 'Team owner', + pronouns: 'he/him', + role: 'Software Engineer', + company: 'Acme Corp', + avatarUrl: 'https://example.com/john.png', + accentColor: '#6366f1', +}; + +const MOCK_MEMBER_USER = { + id: MOCK_MEMBER_ID, + username: 'janedoe', + displayName: 'Jane Doe', + bio: null, + pronouns: null, + role: 'Designer', + company: null, + avatarUrl: null, + accentColor: '#f43f5e', +}; + +const MOCK_PLATFORM_LINKS = [ + { id: 'link-uuid-001', platform: 'github', username: 'johndoe', url: 'https://github.com/johndoe', displayOrder: 0 }, + { id: 'link-uuid-002', platform: 'twitter', username: 'johndoe_', url: 'https://twitter.com/johndoe_', displayOrder: 1 }, +]; + +const MOCK_TEAM = { + id: 'team-uuid-001', + name: 'DevCard Core', + slug: 'devcard-core', + description: 'Building the future of developer cards', + avatarUrl: 'https://example.com/team.png', + ownerId: MOCK_OWNER_ID, + createdAt: new Date('2024-01-01T00:00:00Z'), + updatedAt: new Date('2024-06-01T00:00:00Z'), +}; + +const MOCK_TEAM_WITH_MEMBERS = { + ...MOCK_TEAM, + members: [ + { + id: 'tm-uuid-001', + teamId: MOCK_TEAM.id, + userId: MOCK_OWNER_ID, + role: TeamRole.OWNER, + joinedAt: new Date('2024-01-01T00:00:00Z'), + user: { ...MOCK_OWNER, platformLinks: MOCK_PLATFORM_LINKS }, + }, + { + id: 'tm-uuid-002', + teamId: MOCK_TEAM.id, + userId: MOCK_MEMBER_ID, + role: TeamRole.MEMBER, + joinedAt: new Date('2024-02-01T00:00:00Z'), + user: { ...MOCK_MEMBER_USER, platformLinks: [] }, + }, + ], +}; + +// ─── Prisma mock ────────────────────────────────────────────────────────────── + +const prismaMock = { + team: { + create: vi.fn(), + findUnique: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + }, + teamMember: { + create: vi.fn(), + delete: vi.fn(), + }, + user: { + findUnique: vi.fn(), + }, + $transaction: vi.fn(), +}; + +// ─── App factory ────────────────────────────────────────────────────────────── + +let mockJwtVerify = vi.fn(); + +async function buildApp(): Promise { + const app = Fastify({ logger: false }); + + app.decorate('prisma', prismaMock as unknown as PrismaClient); + + app.decorateRequest('jwtVerify', function () { + return mockJwtVerify(); + }); + + await app.register(teamRoutes); + await app.ready(); + return app; +} + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +function authHeader(): Record { + return { Authorization: 'Bearer mock-token' }; +} + +async function createTeam( + app: FastifyInstance, + body: Record, + authenticated = true, +) { + return app.inject({ + method: 'POST', + url: '/', + headers: authenticated ? authHeader() : {}, + payload: body, + }); +} + +// ─── Test suite ─────────────────────────────────────────────────────────────── + +describe('Teams API', () => { + let app: FastifyInstance; + + beforeEach(async () => { + vi.clearAllMocks(); + mockJwtVerify.mockResolvedValue({ id: MOCK_OWNER_ID }); + app = await buildApp(); + }); + + afterEach(async () => { + await app.close(); + }); + + // ── POST / — create team ────────────────────────────────────────────────── + + describe('POST / — create team', () => { + const validBody = { + name: 'DevCard Core', + description: 'Building the future of developer cards', + avatarUrl: 'https://example.com/team.png', + }; + + it('201 — creates team and auto-adds owner as OWNER member', async () => { + prismaMock.team.findUnique.mockResolvedValue(null); + prismaMock.$transaction.mockImplementation(async (cb: any) => { + return cb({ + team: { create: vi.fn().mockResolvedValue(MOCK_TEAM) }, + teamMember: { create: vi.fn().mockResolvedValue({}) }, + }); + }); + + const res = await createTeam(app, validBody); + + expect(res.statusCode).toBe(201); + const body = res.json(); + expect(body.name).toBe('DevCard Core'); + expect(body.ownerId).toBe(MOCK_OWNER_ID); + expect(body.slug).toBe('devcard-core'); + }); + + it('401 — rejects unauthenticated request', async () => { + mockJwtVerify.mockRejectedValue(new Error('Unauthorized')); + + const res = await createTeam(app, validBody, false); + + expect(res.statusCode).toBe(401); + expect(res.json()).toMatchObject({ error: 'Unauthorized' }); + }); + + it('400 — rejects name shorter than 3 characters', async () => { + const res = await createTeam(app, { ...validBody, name: 'AB' }); + expect(res.statusCode).toBe(400); + }); + + it('400 — rejects name longer than 100 characters', async () => { + const res = await createTeam(app, { ...validBody, name: 'A'.repeat(101) }); + expect(res.statusCode).toBe(400); + }); + + it('400 — rejects invalid avatarUrl', async () => { + const res = await createTeam(app, { ...validBody, avatarUrl: 'not-a-url' }); + expect(res.statusCode).toBe(400); + }); + + it('400 — rejects missing name', async () => { + const { name: _omit, ...bodyWithoutName } = validBody; + const res = await createTeam(app, bodyWithoutName); + expect(res.statusCode).toBe(400); + }); + + it('201 — creates team without optional fields', async () => { + prismaMock.team.findUnique.mockResolvedValue(null); + prismaMock.$transaction.mockImplementation(async (cb: any) => { + return cb({ + team: { create: vi.fn().mockResolvedValue({ ...MOCK_TEAM, description: null, avatarUrl: null }) }, + teamMember: { create: vi.fn().mockResolvedValue({}) }, + }); + }); + + const res = await createTeam(app, { name: 'DevCard Core' }); + expect(res.statusCode).toBe(201); + }); + + it('500 — returns 500 on database failure', async () => { + prismaMock.team.findUnique.mockResolvedValue(null); + prismaMock.$transaction.mockRejectedValue(new Error('DB error')); + + const res = await createTeam(app, validBody); + expect(res.statusCode).toBe(500); + expect(res.json()).toMatchObject({ error: 'Failed to create team' }); + }); + }); + + // ── GET /:slug — public team profile ───────────────────────────────────── + + describe('GET /:slug — public team profile', () => { + it('200 — returns team with members in PublicProfile shape', async () => { + prismaMock.team.findUnique.mockResolvedValue(MOCK_TEAM_WITH_MEMBERS); + + const res = await app.inject({ method: 'GET', url: '/devcard-core' }); + + expect(res.statusCode).toBe(200); + const body = res.json(); + + expect(body.slug).toBe('devcard-core'); + expect(body.ownerId).toBe(MOCK_OWNER_ID); + expect(body.members).toHaveLength(2); + }); + + it('200 — each member has PublicProfile fields and links array', async () => { + prismaMock.team.findUnique.mockResolvedValue(MOCK_TEAM_WITH_MEMBERS); + + const res = await app.inject({ method: 'GET', url: '/devcard-core' }); + const owner = res.json().members[0]; + + expect(owner).toHaveProperty('username', 'johndoe'); + expect(owner).toHaveProperty('displayName', 'John Doe'); + expect(owner).toHaveProperty('accentColor'); + expect(owner).toHaveProperty('links'); + expect(owner.links).toHaveLength(2); + expect(owner.links[0]).toMatchObject({ + platform: 'github', + username: 'johndoe', + url: 'https://github.com/johndoe', + displayOrder: 0, + }); + }); + + it('200 — member has teamRole and joinedAt fields', async () => { + prismaMock.team.findUnique.mockResolvedValue(MOCK_TEAM_WITH_MEMBERS); + + const res = await app.inject({ method: 'GET', url: '/devcard-core' }); + const owner = res.json().members[0]; + + expect(owner).toHaveProperty('teamRole', 'OWNER'); + expect(owner).toHaveProperty('joinedAt'); + }); + + it('200 — does not leak sensitive user fields on members', async () => { + prismaMock.team.findUnique.mockResolvedValue(MOCK_TEAM_WITH_MEMBERS); + + const res = await app.inject({ method: 'GET', url: '/devcard-core' }); + const member = res.json().members[0]; + + expect(member).not.toHaveProperty('email'); + expect(member).not.toHaveProperty('provider'); + expect(member).not.toHaveProperty('providerId'); + }); + + it('200 — works without authentication (public endpoint)', async () => { + mockJwtVerify.mockRejectedValue(new Error('Should not be called')); + prismaMock.team.findUnique.mockResolvedValue(MOCK_TEAM_WITH_MEMBERS); + + const res = await app.inject({ method: 'GET', url: '/devcard-core' }); + + expect(res.statusCode).toBe(200); + expect(mockJwtVerify).not.toHaveBeenCalled(); + }); + + it('404 — returns 404 for unknown slug', async () => { + prismaMock.team.findUnique.mockResolvedValue(null); + + const res = await app.inject({ method: 'GET', url: '/ghost-team' }); + + expect(res.statusCode).toBe(404); + expect(res.json()).toMatchObject({ error: 'Team not found' }); + }); + + it('200 — returns empty members array for a team with no members', async () => { + prismaMock.team.findUnique.mockResolvedValue({ ...MOCK_TEAM, members: [] }); + + const res = await app.inject({ method: 'GET', url: '/devcard-core' }); + + expect(res.statusCode).toBe(200); + expect(res.json().members).toHaveLength(0); + }); + }); + + // ── POST /:slug/members — invite member ─────────────────────────────────── + + describe('POST /:slug/members — invite member (owner only)', () => { + const teamWithOwnerOnly = { + ...MOCK_TEAM, + owner: MOCK_OWNER, + members: [ + { + id: 'tm-uuid-001', + teamId: MOCK_TEAM.id, + userId: MOCK_OWNER_ID, + role: TeamRole.OWNER, + joinedAt: new Date(), + user: MOCK_OWNER, + }, + ], + }; + + it('201 — owner can invite a new member by username', async () => { + prismaMock.team.findUnique.mockResolvedValue(teamWithOwnerOnly); + prismaMock.user.findUnique.mockResolvedValue(MOCK_MEMBER_USER); + prismaMock.teamMember.create.mockResolvedValue({}); + + const res = await app.inject({ + method: 'POST', + url: '/devcard-core/members', + headers: authHeader(), + payload: { username: 'janedoe' }, + }); + + expect(res.statusCode).toBe(201); + expect(prismaMock.teamMember.create).toHaveBeenCalledOnce(); + + const callData = prismaMock.teamMember.create.mock.calls[0][0].data; + expect(callData.userId).toBe(MOCK_MEMBER_ID); + expect(callData.role).toBe(TeamRole.MEMBER); + }); + + it('401 — rejects unauthenticated request', async () => { + mockJwtVerify.mockRejectedValue(new Error('Unauthorized')); + + const res = await app.inject({ + method: 'POST', + url: '/devcard-core/members', + payload: { username: 'janedoe' }, + }); + + expect(res.statusCode).toBe(401); + }); + + it('403 — non-owner cannot invite members', async () => { + mockJwtVerify.mockResolvedValue({ id: MOCK_MEMBER_ID }); + prismaMock.team.findUnique.mockResolvedValue(teamWithOwnerOnly); + + const res = await app.inject({ + method: 'POST', + url: '/devcard-core/members', + headers: authHeader(), + payload: { username: 'someoneelse' }, + }); + + expect(res.statusCode).toBe(403); + expect(prismaMock.teamMember.create).not.toHaveBeenCalled(); + }); + + it('409 — cannot invite a user who is already a member', async () => { + prismaMock.team.findUnique.mockResolvedValue({ + ...teamWithOwnerOnly, + members: [ + ...teamWithOwnerOnly.members, + { + id: 'tm-uuid-002', + teamId: MOCK_TEAM.id, + userId: MOCK_MEMBER_ID, + role: TeamRole.MEMBER, + joinedAt: new Date(), + user: MOCK_MEMBER_USER, + }, + ], + }); + + const res = await app.inject({ + method: 'POST', + url: '/devcard-core/members', + headers: authHeader(), + payload: { username: 'janedoe' }, + }); + + expect(res.statusCode).toBe(409); + expect(prismaMock.teamMember.create).not.toHaveBeenCalled(); + }); + + it('409 — cannot invite the owner (they are already a member)', async () => { + prismaMock.team.findUnique.mockResolvedValue(teamWithOwnerOnly); + + const res = await app.inject({ + method: 'POST', + url: '/devcard-core/members', + headers: authHeader(), + payload: { username: 'johndoe' }, + }); + + expect(res.statusCode).toBe(409); + }); + + it('404 — returns 404 when invited username does not exist', async () => { + prismaMock.team.findUnique.mockResolvedValue(teamWithOwnerOnly); + prismaMock.user.findUnique.mockResolvedValue(null); + + const res = await app.inject({ + method: 'POST', + url: '/devcard-core/members', + headers: authHeader(), + payload: { username: 'ghostuser' }, + }); + + expect(res.statusCode).toBe(404); + }); + + it('404 — returns 404 when team does not exist', async () => { + prismaMock.team.findUnique.mockResolvedValue(null); + + const res = await app.inject({ + method: 'POST', + url: '/ghost-team/members', + headers: authHeader(), + payload: { username: 'janedoe' }, + }); + + expect(res.statusCode).toBe(404); + }); + + it('400 — rejects empty username', async () => { + const res = await app.inject({ + method: 'POST', + url: '/devcard-core/members', + headers: authHeader(), + payload: { username: '' }, + }); + + expect(res.statusCode).toBe(400); + }); + }); + + // ── DELETE /:slug/members/:userId — remove member ───────────────────────── + + describe('DELETE /:slug/members/:userId — remove member', () => { + const teamWithBothMembers = { + ...MOCK_TEAM, + members: [ + { + id: 'tm-uuid-001', + teamId: MOCK_TEAM.id, + userId: MOCK_OWNER_ID, + role: TeamRole.OWNER, + joinedAt: new Date(), + user: MOCK_OWNER, + }, + { + id: 'tm-uuid-002', + teamId: MOCK_TEAM.id, + userId: MOCK_MEMBER_ID, + role: TeamRole.MEMBER, + joinedAt: new Date(), + user: MOCK_MEMBER_USER, + }, + ], + }; + + it('200 — owner can remove a member', async () => { + prismaMock.team.findUnique.mockResolvedValue(teamWithBothMembers); + prismaMock.teamMember.delete.mockResolvedValue({}); + + const res = await app.inject({ + method: 'DELETE', + url: `/devcard-core/members/${MOCK_MEMBER_ID}`, + headers: authHeader(), + }); + + expect(res.statusCode).toBe(200); + const deleteArg = prismaMock.teamMember.delete.mock.calls[0][0].where; + expect(deleteArg).toMatchObject({ + userId_teamId: { + teamId: MOCK_TEAM.id, + userId: MOCK_MEMBER_ID, + }, + }); + }); + + it('200 — member can self-remove (leave team)', async () => { + mockJwtVerify.mockResolvedValue({ id: MOCK_MEMBER_ID }); + prismaMock.team.findUnique.mockResolvedValue(teamWithBothMembers); + prismaMock.teamMember.delete.mockResolvedValue({}); + + const res = await app.inject({ + method: 'DELETE', + url: `/devcard-core/members/${MOCK_MEMBER_ID}`, + headers: authHeader(), + }); + + expect(res.statusCode).toBe(200); + }); + + it('403 — owner cannot leave their own team', async () => { + prismaMock.team.findUnique.mockResolvedValue(teamWithBothMembers); + + const res = await app.inject({ + method: 'DELETE', + url: `/devcard-core/members/${MOCK_OWNER_ID}`, + headers: authHeader(), + }); + + expect(res.statusCode).toBe(403); + expect(prismaMock.teamMember.delete).not.toHaveBeenCalled(); + }); + + it('403 — outsider cannot remove another member', async () => { + mockJwtVerify.mockResolvedValue({ id: MOCK_OUTSIDER_ID }); + prismaMock.team.findUnique.mockResolvedValue(teamWithBothMembers); + + const res = await app.inject({ + method: 'DELETE', + url: `/devcard-core/members/${MOCK_MEMBER_ID}`, + headers: authHeader(), + }); + + expect(res.statusCode).toBe(403); + expect(prismaMock.teamMember.delete).not.toHaveBeenCalled(); + }); + + it('401 — rejects unauthenticated request', async () => { + mockJwtVerify.mockRejectedValue(new Error('Unauthorized')); + + const res = await app.inject({ + method: 'DELETE', + url: `/devcard-core/members/${MOCK_MEMBER_ID}`, + }); + + expect(res.statusCode).toBe(401); + }); + + it('404 — returns 404 when team does not exist', async () => { + prismaMock.team.findUnique.mockResolvedValue(null); + + const res = await app.inject({ + method: 'DELETE', + url: `/ghost-team/members/${MOCK_MEMBER_ID}`, + headers: authHeader(), + }); + + expect(res.statusCode).toBe(404); + }); + + it('404 — returns 404 when userId is not a team member', async () => { + prismaMock.team.findUnique.mockResolvedValue(teamWithBothMembers); + + const res = await app.inject({ + method: 'DELETE', + url: `/devcard-core/members/${MOCK_OUTSIDER_ID}`, + headers: authHeader(), + }); + + expect(res.statusCode).toBe(404); + }); + }); + + // ── PATCH /:slug — update team ──────────────────────────────────────────── + + describe('PATCH /:slug — update team (owner only)', () => { + it('200 — owner can update name, description, avatarUrl', async () => { + prismaMock.team.findUnique.mockResolvedValue(MOCK_TEAM); + prismaMock.team.update.mockResolvedValue({ ...MOCK_TEAM, name: 'New Name' }); + + const res = await app.inject({ + method: 'PATCH', + url: '/devcard-core', + headers: authHeader(), + payload: { name: 'New Name' }, + }); + + expect(res.statusCode).toBe(200); + expect(res.json().name).toBe('New Name'); + }); + + it('403 — non-owner cannot update team', async () => { + mockJwtVerify.mockResolvedValue({ id: MOCK_MEMBER_ID }); + prismaMock.team.findUnique.mockResolvedValue(MOCK_TEAM); + + const res = await app.inject({ + method: 'PATCH', + url: '/devcard-core', + headers: authHeader(), + payload: { name: 'Hijacked Name' }, + }); + + expect(res.statusCode).toBe(403); + expect(prismaMock.team.update).not.toHaveBeenCalled(); + }); + + it('401 — rejects unauthenticated request', async () => { + mockJwtVerify.mockRejectedValue(new Error('Unauthorized')); + + const res = await app.inject({ + method: 'PATCH', + url: '/devcard-core', + payload: { name: 'New Name' }, + }); + + expect(res.statusCode).toBe(401); + }); + + it('400 — rejects empty body (at least one field required)', async () => { + const res = await app.inject({ + method: 'PATCH', + url: '/devcard-core', + headers: authHeader(), + payload: {}, + }); + + expect(res.statusCode).toBe(400); + }); + + it('400 — rejects invalid avatarUrl', async () => { + const res = await app.inject({ + method: 'PATCH', + url: '/devcard-core', + headers: authHeader(), + payload: { avatarUrl: 'not-a-url' }, + }); + + expect(res.statusCode).toBe(400); + }); + + it('404 — returns 404 for unknown slug', async () => { + prismaMock.team.findUnique.mockResolvedValue(null); + + const res = await app.inject({ + method: 'PATCH', + url: '/ghost-team', + headers: authHeader(), + payload: { name: 'New Name' }, + }); + + expect(res.statusCode).toBe(404); + }); + }); + + // ── DELETE /:slug — delete team ─────────────────────────────────────────── + + describe('DELETE /:slug — delete team (owner only)', () => { + it('200 — owner can delete team', async () => { + prismaMock.team.findUnique.mockResolvedValue(MOCK_TEAM); + prismaMock.team.delete.mockResolvedValue({}); + + const res = await app.inject({ + method: 'DELETE', + url: '/devcard-core', + headers: authHeader(), + }); + + expect(res.statusCode).toBe(200); + expect(prismaMock.team.delete).toHaveBeenCalledOnce(); + }); + + it('403 — non-owner cannot delete team', async () => { + mockJwtVerify.mockResolvedValue({ id: MOCK_MEMBER_ID }); + prismaMock.team.findUnique.mockResolvedValue(MOCK_TEAM); + + const res = await app.inject({ + method: 'DELETE', + url: '/devcard-core', + headers: authHeader(), + }); + + expect(res.statusCode).toBe(403); + expect(prismaMock.team.delete).not.toHaveBeenCalled(); + }); + + it('401 — rejects unauthenticated request', async () => { + mockJwtVerify.mockRejectedValue(new Error('Unauthorized')); + + const res = await app.inject({ + method: 'DELETE', + url: '/devcard-core', + }); + + expect(res.statusCode).toBe(401); + }); + + it('404 — returns 404 for unknown slug', async () => { + prismaMock.team.findUnique.mockResolvedValue(null); + + const res = await app.inject({ + method: 'DELETE', + url: '/ghost-team', + headers: authHeader(), + }); + + expect(res.statusCode).toBe(404); + }); + + it('500 — returns 500 on database failure', async () => { + prismaMock.team.findUnique.mockResolvedValue(MOCK_TEAM); + prismaMock.team.delete.mockRejectedValue(new Error('DB error')); + + const res = await app.inject({ + method: 'DELETE', + url: '/devcard-core', + headers: authHeader(), + }); + + expect(res.statusCode).toBe(500); + }); + }); + + // ── GET /:slug/qr — QR code ─────────────────────────────────────────────── + + describe('GET /:slug/qr — QR code', () => { + it('200 — returns PNG image for valid slug', async () => { + prismaMock.team.findUnique.mockResolvedValue(MOCK_TEAM); + + const res = await app.inject({ + method: 'GET', + url: '/devcard-core/qr', + }); + + expect(res.statusCode).toBe(200); + expect(res.headers['content-type']).toMatch('image/png'); + }); + + it('200 — encodes correct devcard.dev URL in QR', async () => { + prismaMock.team.findUnique.mockResolvedValue(MOCK_TEAM); + + const res = await app.inject({ + method: 'GET', + url: '/devcard-core/qr', + }); + + expect(res.statusCode).toBe(200); + expect(res.rawPayload.length).toBeGreaterThan(0); + }); + + it('200 — works without authentication (public endpoint)', async () => { + mockJwtVerify.mockRejectedValue(new Error('Should not be called')); + prismaMock.team.findUnique.mockResolvedValue(MOCK_TEAM); + + const res = await app.inject({ + method: 'GET', + url: '/devcard-core/qr', + }); + + expect(res.statusCode).toBe(200); + expect(mockJwtVerify).not.toHaveBeenCalled(); + }); + + it('404 — returns 404 for unknown slug', async () => { + prismaMock.team.findUnique.mockResolvedValue(null); + + const res = await app.inject({ + method: 'GET', + url: '/ghost-team/qr', + }); + + expect(res.statusCode).toBe(404); + }); + }); +}); \ No newline at end of file diff --git a/apps/backend/src/__tests__/validateEnv.test.ts b/apps/backend/src/__tests__/validateEnv.test.ts new file mode 100644 index 00000000..eb0574bd --- /dev/null +++ b/apps/backend/src/__tests__/validateEnv.test.ts @@ -0,0 +1,136 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { validateEnv } from '../utils/validateEnv.js'; + +// ── helpers ────────────────────────────────────────────────────────────────── + +/** + * Replaces process.exit with a throwing stub for the duration of the test so + * that a failing validateEnv() call does not terminate the test process. + * Returns the spy so callers can assert the exit code. + */ +function stubExit() { + return vi.spyOn(process, 'exit').mockImplementation((code?: number | string) => { + throw new Error(`process.exit(${code})`); + }) as unknown as ReturnType; +} + +// ── test suite ──────────────────────────────────────────────────────────────── + +describe('validateEnv', () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + }); + + // ─── JWT_SECRET ────────────────────────────────────────────────────────── + + it('exits with code 1 when JWT_SECRET is absent', () => { + vi.stubEnv('JWT_SECRET', undefined as unknown as string); + vi.stubEnv('ENCRYPTION_KEY', 'a-valid-encryption-key'); + const exit = stubExit(); + + expect(() => validateEnv()).toThrow('process.exit(1)'); + expect(exit).toHaveBeenCalledWith(1); + }); + + it('exits with code 1 when JWT_SECRET is an empty string', () => { + vi.stubEnv('JWT_SECRET', ''); + vi.stubEnv('ENCRYPTION_KEY', 'a-valid-encryption-key'); + stubExit(); + + expect(() => validateEnv()).toThrow('process.exit(1)'); + }); + + it('exits with code 1 when JWT_SECRET is the known insecure default in production', () => { + vi.stubEnv('JWT_SECRET', 'dev-secret-change-me'); + vi.stubEnv('ENCRYPTION_KEY', 'a-valid-encryption-key'); + vi.stubEnv('NODE_ENV', 'production'); + stubExit(); + + expect(() => validateEnv()).toThrow('process.exit(1)'); + }); + + it('allows the known insecure default in non-production (development)', () => { + // The known-insecure check is production-only so local development still + // works with the default value without requiring a full secrets setup. + vi.stubEnv('JWT_SECRET', 'dev-secret-change-me'); + vi.stubEnv('ENCRYPTION_KEY', 'a-valid-encryption-key'); + vi.stubEnv('NODE_ENV', 'development'); + + // Must not throw / call process.exit + expect(() => validateEnv()).not.toThrow(); + }); + + it('allows the known insecure default when NODE_ENV is not set', () => { + vi.stubEnv('JWT_SECRET', 'dev-secret-change-me'); + vi.stubEnv('ENCRYPTION_KEY', 'a-valid-encryption-key'); + vi.stubEnv('NODE_ENV', undefined as unknown as string); + + expect(() => validateEnv()).not.toThrow(); + }); + + // ─── ENCRYPTION_KEY ────────────────────────────────────────────────────── + + it('exits with code 1 when ENCRYPTION_KEY is absent', () => { + vi.stubEnv('JWT_SECRET', 'a-valid-jwt-secret-that-is-sufficiently-long'); + vi.stubEnv('ENCRYPTION_KEY', undefined as unknown as string); + stubExit(); + + expect(() => validateEnv()).toThrow('process.exit(1)'); + }); + + it('exits with code 1 when ENCRYPTION_KEY is an empty string', () => { + vi.stubEnv('JWT_SECRET', 'a-valid-jwt-secret-that-is-sufficiently-long'); + vi.stubEnv('ENCRYPTION_KEY', ''); + stubExit(); + + expect(() => validateEnv()).toThrow('process.exit(1)'); + }); + + // ─── Multiple failures ──────────────────────────────────────────────────── + + it('reports both missing secrets in a single exit call', () => { + vi.stubEnv('JWT_SECRET', undefined as unknown as string); + vi.stubEnv('ENCRYPTION_KEY', undefined as unknown as string); + const exit = stubExit(); + + expect(() => validateEnv()).toThrow('process.exit(1)'); + // A single exit — not one per error — so operators fix everything in one deploy. + expect(exit).toHaveBeenCalledTimes(1); + expect(exit).toHaveBeenCalledWith(1); + }); + + // ─── Happy path ────────────────────────────────────────────────────────── + + it('passes when both secrets are valid in development', () => { + vi.stubEnv('JWT_SECRET', 'a-valid-jwt-secret-that-is-sufficiently-long'); + vi.stubEnv('ENCRYPTION_KEY', 'a-valid-32-char-encryption-key!!'); + vi.stubEnv('NODE_ENV', 'development'); + + expect(() => validateEnv()).not.toThrow(); + }); + + it('passes when both secrets are valid in production', () => { + vi.stubEnv('JWT_SECRET', 'a-long-random-production-jwt-secret-with-enough-entropy'); + vi.stubEnv('ENCRYPTION_KEY', 'a-64-char-hex-encryption-key-for-aes-256-gcm-0000000000000000'); + vi.stubEnv('NODE_ENV', 'production'); + + expect(() => validateEnv()).not.toThrow(); + }); + + // ─── No secret leakage ─────────────────────────────────────────────────── + + it('does not log the value of JWT_SECRET when reporting errors', () => { + const secretValue = 'super-secret-value-that-must-not-appear-in-logs'; + vi.stubEnv('JWT_SECRET', undefined as unknown as string); + vi.stubEnv('ENCRYPTION_KEY', 'a-valid-encryption-key'); + stubExit(); + + const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + expect(() => validateEnv()).toThrow('process.exit(1)'); + + const allOutput = errSpy.mock.calls.flat().join(' '); + expect(allOutput).not.toContain(secretValue); + }); +}); diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index 8e8cf381..06b87205 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -1,26 +1,37 @@ -import Fastify from 'fastify'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import cookie from '@fastify/cookie'; import cors from '@fastify/cors'; import helmet from '@fastify/helmet'; import jwt from '@fastify/jwt'; -import cookie from '@fastify/cookie'; import multipart from '@fastify/multipart'; +import rateLimit from '@fastify/rate-limit'; import fastifyStatic from '@fastify/static'; -import path from 'path'; -import { fileURLToPath } from 'url'; +import Fastify, {type FastifyInstance} from 'fastify'; import { prismaPlugin } from './plugins/prisma.js'; import { redisPlugin } from './plugins/redis.js'; +import { analyticsRoutes } from './routes/analytics.js'; import { authRoutes } from './routes/auth.js'; -import { profileRoutes } from './routes/profiles.js'; import { cardRoutes } from './routes/cards.js'; -import { publicRoutes } from './routes/public.js'; -import { followRoutes } from './routes/follow.js'; import { connectRoutes } from './routes/connect.js'; -import { analyticsRoutes } from './routes/analytics.js'; +import { eventRoutes } from './routes/event.js'; +import { followRoutes } from './routes/follow.js'; +import { nfcRoutes } from './routes/nfc.js'; +import { profileRoutes } from './routes/profiles.js'; +import { publicRoutes } from './routes/public.js'; +import { validateEnv } from './utils/validateEnv.js'; +import { teamRoutes } from './routes/team.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -export async function buildApp() { +export async function buildApp():Promise { + // Validate all required secrets before registering any plugin. + // If validation fails the process exits here — no partially-initialised + // auth state can exist because Fastify is not yet instantiated. + validateEnv(); + const app = Fastify({ logger: { level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', @@ -55,28 +66,34 @@ export async function buildApp() { }); await app.register(jwt, { - secret: process.env.JWT_SECRET || 'dev-secret-change-me', + // validateEnv() above guarantees JWT_SECRET is present and safe. + secret: process.env.JWT_SECRET!, }); await app.register(cookie); await app.register(multipart, { limits: { fileSize: 5 * 1024 * 1024 } }); // 5MB - - // Static file serving for uploads - await app.register(fastifyStatic, { - root: path.join(__dirname, '..', 'uploads'), - prefix: '/uploads/', - decorateReply: false, + await app.register(rateLimit, { + max: 100, + timeWindow: '1 minute', }); +// Files must be served through authenticated route handlers +// with ownership validation. + // ─── Database & Cache Plugins ─── - await app.register(prismaPlugin); + if (process.env.NODE_ENV !== 'test') { + await app.register(prismaPlugin); //change +} + if (process.env.NODE_ENV !== 'test') { await app.register(redisPlugin); - +} // ─── Auth Decorator ─── app.decorate('authenticate', async function (request: any, reply: any) { try { - await request.jwtVerify(); - } catch (err) { + // Ensure the verified payload is assigned to `request.user` like the original plugin. + const payload = await request.jwtVerify(); + if (payload) request.user = payload; + } catch (error) { reply.status(401).send({ error: 'Unauthorized' }); } }); @@ -85,17 +102,38 @@ export async function buildApp() { await app.register(authRoutes, { prefix: '/auth' }); await app.register(profileRoutes, { prefix: '/api/profiles' }); await app.register(cardRoutes, { prefix: '/api/cards' }); + // Public routes: standardise on `/api/u` (remove duplicate `/api/public`). await app.register(publicRoutes, { prefix: '/api/u' }); await app.register(followRoutes, { prefix: '/api/follow' }); await app.register(connectRoutes, { prefix: '/api/connect' }); await app.register(analyticsRoutes, { prefix: '/api/analytics' }); + await app.register(nfcRoutes, { prefix: '/api/nfc' }); + await app.register(eventRoutes, {prefix: '/api/events'}) + await app.register(teamRoutes, {prefix: '/api/teams'}) + // ─── Health Check ─── - app.get('/health', async () => ({ - status: 'ok', - timestamp: new Date().toISOString(), - service: 'devcard-api', - })); +type HealthResponse = { + status: 'ok'; +}; + +app.get('/health', async (): Promise => { + return { status: 'ok' }; +}); + // Centralized error handler: log and return a consistent 500 shape for unhandled errors. + app.setErrorHandler((error, request, reply) => { + app.log.error({ err: error }, 'Unhandled error'); + // Also print to console to aid test diagnostics when logger is disabled. + // This helps surface stack traces in CI/test runs. + // eslint-disable-next-line no-console + console.error(error); + // If headers were already sent, fall back to default behaviour. + if (reply.sent) { + return; + } + // Keep response shape consistent across the API. + reply.status(500).send({ error: 'Internal server error' }); + }); return app; } diff --git a/apps/backend/src/env.ts b/apps/backend/src/env.ts index 5902853d..7d841d9c 100644 --- a/apps/backend/src/env.ts +++ b/apps/backend/src/env.ts @@ -8,8 +8,9 @@ const envPath = path.resolve(__dirname, '../../../.env'); const result = dotenv.config({ path: envPath }); if (result.error) { - console.error('❌ Failed to load .env from:', envPath); - console.error(result.error); + // Keep failing fast but avoid leaking via console in production code paths. + // This file runs before the Fastify logger is available; throw so the process exits. + throw result.error; } else { - console.log('✅ Loaded .env from:', envPath); + // .env loaded successfully } diff --git a/apps/backend/src/plugins/prisma.ts b/apps/backend/src/plugins/prisma.ts index 98e7f798..f6ebede8 100644 --- a/apps/backend/src/plugins/prisma.ts +++ b/apps/backend/src/plugins/prisma.ts @@ -1,10 +1,14 @@ import fp from 'fastify-plugin'; import { PrismaClient } from '@prisma/client'; -import type { FastifyInstance } from 'fastify'; +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; declare module 'fastify' { interface FastifyInstance { prisma: PrismaClient; + authenticate( + request: FastifyRequest, + reply: FastifyReply + ): Promise; } } diff --git a/apps/backend/src/plugins/redis.ts b/apps/backend/src/plugins/redis.ts index c7b6f94d..864b112f 100644 --- a/apps/backend/src/plugins/redis.ts +++ b/apps/backend/src/plugins/redis.ts @@ -17,7 +17,7 @@ export const redisPlugin = fp(async (app: FastifyInstance) => { try { await redis.connect(); app.log.info('🔴 Redis connected'); - } catch (err) { + } catch (error) { app.log.warn('⚠️ Redis connection failed — running without cache'); } diff --git a/apps/backend/src/routes/analytics.ts b/apps/backend/src/routes/analytics.ts index e9a75bb9..a975424f 100644 --- a/apps/backend/src/routes/analytics.ts +++ b/apps/backend/src/routes/analytics.ts @@ -1,101 +1,162 @@ -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; - -export async function analyticsRoutes(app: FastifyInstance) { - - app.get('/overview', { - preHandler: [app.authenticate], - }, async (request: FastifyRequest, reply: FastifyReply) => { - const userId = (request.user as any).id; - - const today = new Date(); - today.setHours(0, 0, 0, 0); - - const [totalViews, viewsToday, totalFollows, recentViews] = await Promise.all([ - // Total views of this user's cards/profile - app.prisma.cardView.count({ - where: { ownerId: userId }, - }), - // Views today - app.prisma.cardView.count({ - where: { ownerId: userId, createdAt: { gte: today } }, - }), - // Follows performed BY this user - app.prisma.followLog.count({ - where: { followerId: userId, status: 'success' }, - }), - // Recent views (last 5) - app.prisma.cardView.findMany({ - where: { ownerId: userId }, - orderBy: { createdAt: 'desc' }, - take: 5, - include: { - viewer: { - select: { displayName: true, avatarUrl: true }, +import type { + FastifyInstance, + FastifyRequest, + FastifyReply, +} from 'fastify'; + +export async function analyticsRoutes( + app: FastifyInstance +): Promise { + + app.get( + '/overview', + { + // eslint-disable-next-line @typescript-eslint/unbound-method + preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }], + }, + async ( + request: FastifyRequest, + _reply: FastifyReply + ) => { + const userId = (request.user as any).id; + const username = (request.user as any).username; + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const [totalViews, viewsToday, totalFollows, recentViews] = await Promise.all([ + // Total views of this user's cards/profile + app.prisma.cardView.count({ + where: { ownerId: userId }, + }), + + // Views today + app.prisma.cardView.count({ + where: { + ownerId: userId, + createdAt: { gte: today }, }, - card: { - select: { title: true }, + }), + + // Follows performed BY this user + app.prisma.followLog.count({ + where: { + targetUsername: username, + status: 'success', }, - }, - }), - ]); - - // Count unique viewers - // In raw SQL this is `SELECT COUNT(DISTINCT viewer_id) FROM card_views WHERE owner_id = ?` - // Prisma group-by as workaround: - const uniqueViewersQuery = await app.prisma.cardView.groupBy({ - by: ['viewerId', 'viewerIp'], - where: { ownerId: userId }, - }); - const uniqueViewers = uniqueViewersQuery.length; - - return { - totalViews, - viewsToday, - totalFollows, - uniqueViewers, - recentViews, - }; - }); - - app.get('/views', { - preHandler: [app.authenticate], - }, async (request: FastifyRequest<{ Querystring: { page?: string, cardId?: string } }>, reply: FastifyReply) => { - const userId = (request.user as any).id; - const page = parseInt(request.query.page || '1', 10); - const limit = 20; - const skip = (page - 1) * limit; - - const whereClause: any = { ownerId: userId }; - if (request.query.cardId) { - whereClause.cardId = request.query.cardId; - } + }), - const [total, views] = await Promise.all([ - app.prisma.cardView.count({ where: whereClause }), - app.prisma.cardView.findMany({ - where: whereClause, - orderBy: { createdAt: 'desc' }, - skip, - take: limit, - include: { - viewer: { - select: { id: true, username: true, displayName: true, avatarUrl: true }, + // Recent views (last 5) + app.prisma.cardView.findMany({ + where: { ownerId: userId }, + orderBy: { createdAt: 'desc' }, + take: 5, + include: { + viewer: { + select: { + displayName: true, + avatarUrl: true, + }, + }, + card: { + select: { + title: true, + }, + }, }, - card: { - select: { id: true, title: true }, + }), + ]); + + // Count unique viewers + // In raw SQL this is `SELECT COUNT(DISTINCT viewer_id) FROM card_views WHERE owner_id = ?` + // Prisma group-by as workaround: + const uniqueViewersQuery = + await app.prisma.cardView.groupBy({ + by: ['viewerId', 'viewerIp'], + where: { ownerId: userId }, + }); + + const uniqueViewers = uniqueViewersQuery.length; + + return { + totalViews, + viewsToday, + totalFollows, + uniqueViewers, + recentViews, + }; + } + ); + + app.get<{ + Querystring: { + page?: string; + cardId?: string; + }; + }>( + '/views', + { + // eslint-disable-next-line @typescript-eslint/unbound-method + preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }], + }, + async ( + request: FastifyRequest<{ + Querystring: { + page?: string; + cardId?: string; + }; + }>, + _reply: FastifyReply + ) => { + const userId = (request.user as any).id; + const page = parseInt(request.query.page || '1', 10); + const limit = 20; + const skip = (page - 1) * limit; + + const whereClause: any = { ownerId: userId }; + + if (request.query.cardId) { + whereClause.cardId = request.query.cardId; + } + + const [total, views] = await Promise.all([ + app.prisma.cardView.count({ + where: whereClause, + }), + + app.prisma.cardView.findMany({ + where: whereClause, + orderBy: { createdAt: 'desc' }, + skip, + take: limit, + include: { + viewer: { + select: { + id: true, + username: true, + displayName: true, + avatarUrl: true, + }, + }, + card: { + select: { + id: true, + title: true, + }, + }, }, + }), + ]); + + return { + data: views, + meta: { + total, + page, + limit, + totalPages: Math.ceil(total / limit), }, - }), - ]); - - return { - data: views, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - }); -} + }; + } + ); +} \ No newline at end of file diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts index e12f10af..c14949e1 100644 --- a/apps/backend/src/routes/auth.ts +++ b/apps/backend/src/routes/auth.ts @@ -1,4 +1,6 @@ import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { encrypt } from '../utils/encryption.js'; +import { buildOAuthState, getMobileRedirectUri } from '../services/authService.js'; const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize'; const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'; @@ -13,12 +15,32 @@ interface OAuthCallbackQuery { } export async function authRoutes(app: FastifyInstance) { - // ─── GitHub OAuth ─── + // Developer login bypass (development only) + if (process.env.NODE_ENV !== 'production') { + app.post('/dev-login', async (request: FastifyRequest, reply: FastifyReply) => { + const user = await app.prisma.user.findUnique({ where: { username: 'devcard-demo' } }); + if (!user) { + return reply.status(404).send({ error: 'Demo user not seeded' }); + } + const token = app.jwt.sign({ id: user.id, username: user.username }, { expiresIn: '30d' }); + return { token }; + }); + } + // GitHub OAuth start app.get('/github', async (request: FastifyRequest, reply: FastifyReply) => { const redirectUri = `${process.env.BACKEND_URL}/auth/github/callback`; const clientState = (request.query as any).state || ''; - const state = clientState ? `${clientState}_${generateState()}` : generateState(); + const mobileRedirectUri = (request.query as any).mobile_redirect_uri || ''; + const state = buildOAuthState(clientState, mobileRedirectUri); + + reply.setCookie('oauth_state', state, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + maxAge: 10 * 60, + }); const params = new URLSearchParams({ client_id: (process.env.GITHUB_CLIENT_ID || '').trim(), @@ -26,26 +48,29 @@ export async function authRoutes(app: FastifyInstance) { scope: 'read:user user:email', state, }); + const authUrl = `${GITHUB_AUTH_URL}?${params}`; - console.log('--- GITHUB OAUTH REDIRECT ---'); - console.log('URL:', authUrl); + app.log.debug({ provider: 'github' }, 'OAuth redirect initiated'); return reply.redirect(authUrl); }); + // GitHub OAuth callback app.get('/github/callback', async (request: FastifyRequest<{ Querystring: OAuthCallbackQuery }>, reply: FastifyReply) => { - const { code } = request.query; + const { code, state } = request.query; + const storedState = request.cookies?.oauth_state; + if (!state || !storedState || state !== storedState) { + return reply.status(400).send({ error: 'Invalid or missing OAuth state — possible CSRF attack' }); + } + reply.clearCookie('oauth_state', { path: '/' }); + if (!code) { return reply.status(400).send({ error: 'Missing authorization code' }); } try { - // Exchange code for token const tokenRes = await fetch(GITHUB_TOKEN_URL, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ client_id: (process.env.GITHUB_CLIENT_ID || '').trim(), client_secret: (process.env.GITHUB_CLIENT_SECRET || '').trim(), @@ -53,20 +78,16 @@ export async function authRoutes(app: FastifyInstance) { redirect_uri: `${process.env.BACKEND_URL}/auth/github/callback`, }), }); - const tokenData = (await tokenRes.json()) as any; + const tokenData = (await tokenRes.json()) as any; if (tokenData.error) { - app.log.error('GitHub token error:', tokenData); + app.log.error({ tokenData }, 'GitHub token error'); return reply.status(400).send({ error: 'Failed to authenticate with GitHub' }); } - // Fetch user profile - const userRes = await fetch(GITHUB_USER_URL, { - headers: { Authorization: `Bearer ${tokenData.access_token}` }, - }); + const userRes = await fetch(GITHUB_USER_URL, { headers: { Authorization: `Bearer ${tokenData.access_token}` } }); const githubUser = (await userRes.json()) as any; - // Fetch email if not public let email = githubUser.email; if (!email) { const emailsRes = await fetch('https://api.github.com/user/emails', { @@ -77,14 +98,8 @@ export async function authRoutes(app: FastifyInstance) { email = primary?.email || emails[0]?.email; } - // Upsert user const user = await app.prisma.user.upsert({ - where: { - provider_providerId: { - provider: 'github', - providerId: String(githubUser.id), - }, - }, + where: { provider_providerId: { provider: 'github', providerId: String(githubUser.id) } }, update: { email: email || `${githubUser.login}@github.local`, displayName: githubUser.name || githubUser.login, @@ -102,49 +117,53 @@ export async function authRoutes(app: FastifyInstance) { }, }); - // Save the authentication token for 'user:email read:user' so we have a basic platform connection - const encryptedToken = (app as any).encryption ? (app as any).encryption.encrypt(tokenData.access_token) : tokenData.access_token; - - await app.prisma.oAuthToken.upsert({ - where: { userId_platform: { userId: user.id, platform: 'github' } }, - update: { accessToken: encryptedToken, scopes: 'read:user user:email' }, - create: { userId: user.id, platform: 'github', accessToken: encryptedToken, scopes: 'read:user user:email' }, - }); + try { + const encryptedToken = encrypt(tokenData.access_token); + await app.prisma.oAuthToken.upsert({ + where: { userId_platform: { userId: user.id, platform: 'github' } }, + update: { accessToken: encryptedToken, scopes: 'read:user user:email' }, + create: { userId: user.id, platform: 'github', accessToken: encryptedToken, scopes: 'read:user user:email' }, + }); + } catch (err) { + app.log.error({ err, userId: user.id }, 'Failed to persist GitHub OAuth token — authentication proceeds'); + } - // Generate JWT - const token = app.jwt.sign( - { id: user.id, username: user.username }, - { expiresIn: '30d' } - ); + const token = app.jwt.sign({ id: user.id, username: user.username }, { expiresIn: '30d' }); - // For mobile app: redirect with token as query param - const mobileRedirect = process.env.MOBILE_REDIRECT_URI; if (request.query.state?.startsWith('mobile_')) { - return reply.redirect(`${mobileRedirect}?token=${token}`); + const mobileRedirect = getMobileRedirectUri(request.query.state) || process.env.MOBILE_REDIRECT_URI; + return reply.redirect(`${mobileRedirect}#token=${token}`); } - // For web: set cookie and redirect reply.setCookie('token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', - maxAge: 30 * 24 * 60 * 60, // 30 days + maxAge: 30 * 24 * 60 * 60, }); return reply.redirect(`${process.env.PUBLIC_APP_URL}/dashboard`); - } catch (err) { - app.log.error('GitHub auth error:', err); + } catch (error) { + app.log.error({ error }, 'GitHub auth error'); return reply.status(500).send({ error: 'Authentication failed' }); } }); - // ─── Google OAuth ─── - + // Google OAuth start app.get('/google', async (request: FastifyRequest, reply: FastifyReply) => { const redirectUri = `${process.env.BACKEND_URL}/auth/google/callback`; const clientState = (request.query as any).state || ''; - const state = clientState ? `${clientState}_${generateState()}` : generateState(); + const mobileRedirectUri = (request.query as any).mobile_redirect_uri || ''; + const state = buildOAuthState(clientState, mobileRedirectUri); + + reply.setCookie('oauth_state', state, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + maxAge: 10 * 60, + }); const params = new URLSearchParams({ client_id: (process.env.GOOGLE_CLIENT_ID || '').trim(), @@ -154,14 +173,22 @@ export async function authRoutes(app: FastifyInstance) { state, access_type: 'offline', }); + const authUrl = `${GOOGLE_AUTH_URL}?${params}`; - console.log('--- GOOGLE OAUTH REDIRECT ---'); - console.log('URL:', authUrl); + app.log.debug({ provider: 'google' }, 'OAuth redirect initiated'); return reply.redirect(authUrl); }); + // Google callback app.get('/google/callback', async (request: FastifyRequest<{ Querystring: OAuthCallbackQuery }>, reply: FastifyReply) => { - const { code } = request.query; + const { code, state } = request.query; + + const storedState = request.cookies?.oauth_state; + if (!state || !storedState || state !== storedState) { + return reply.status(400).send({ error: 'Invalid or missing OAuth state — possible CSRF attack' }); + } + reply.clearCookie('oauth_state', { path: '/' }); + if (!code) { return reply.status(400).send({ error: 'Missing authorization code' }); } @@ -178,33 +205,21 @@ export async function authRoutes(app: FastifyInstance) { grant_type: 'authorization_code', }), }); - const tokenData = (await tokenRes.json()) as any; + const tokenData = (await tokenRes.json()) as any; if (tokenData.error) { - app.log.error('Google token error:', tokenData); + app.log.error({ tokenData }, 'Google token error'); return reply.status(400).send({ error: 'Failed to authenticate with Google' }); } - const userRes = await fetch(GOOGLE_USER_URL, { - headers: { Authorization: `Bearer ${tokenData.access_token}` }, - }); + const userRes = await fetch(GOOGLE_USER_URL, { headers: { Authorization: `Bearer ${tokenData.access_token}` } }); const googleUser = (await userRes.json()) as any; - // Generate username from email const baseUsername = googleUser.email.split('@')[0].replace(/[^a-zA-Z0-9_-]/g, ''); const user = await app.prisma.user.upsert({ - where: { - provider_providerId: { - provider: 'google', - providerId: googleUser.id, - }, - }, - update: { - email: googleUser.email, - displayName: googleUser.name || baseUsername, - avatarUrl: googleUser.picture, - }, + where: { provider_providerId: { provider: 'google', providerId: googleUser.id } }, + update: { email: googleUser.email, displayName: googleUser.name || baseUsername, avatarUrl: googleUser.picture }, create: { email: googleUser.email, username: `${baseUsername}_${Date.now().toString(36)}`, @@ -215,14 +230,11 @@ export async function authRoutes(app: FastifyInstance) { }, }); - const token = app.jwt.sign( - { id: user.id, username: user.username }, - { expiresIn: '30d' } - ); + const token = app.jwt.sign({ id: user.id, username: user.username }, { expiresIn: '30d' }); if (request.query.state?.startsWith('mobile_')) { - const mobileRedirect = process.env.MOBILE_REDIRECT_URI; - return reply.redirect(`${mobileRedirect}?token=${token}`); + const mobileRedirect = getMobileRedirectUri(request.query.state) || process.env.MOBILE_REDIRECT_URI; + return reply.redirect(`${mobileRedirect}#token=${token}`); } reply.setCookie('token', token, { @@ -234,17 +246,19 @@ export async function authRoutes(app: FastifyInstance) { }); return reply.redirect(`${process.env.PUBLIC_APP_URL}/dashboard`); - } catch (err) { - app.log.error('Google auth error:', err); + } catch (error) { + app.log.error({ error }, 'Google auth error'); return reply.status(500).send({ error: 'Authentication failed' }); } }); - // ─── Current User ─── - - app.get('/me', { - preHandler: [app.authenticate], - }, async (request: FastifyRequest, reply: FastifyReply) => { + // Current user + app.get('/me', { preHandler: [async (request, reply) => { + const server = request.server as any; + if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } + if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } + try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + }] }, async (request: FastifyRequest, reply: FastifyReply) => { const userId = (request.user as any).id; const user = await app.prisma.user.findUnique({ where: { id: userId }, @@ -260,9 +274,7 @@ export async function authRoutes(app: FastifyInstance) { avatarUrl: true, accentColor: true, createdAt: true, - oauthTokens: { - select: { platform: true, scopes: true, createdAt: true }, - }, + oauthTokens: { select: { platform: true, scopes: true, createdAt: true } }, }, }); @@ -271,21 +283,11 @@ export async function authRoutes(app: FastifyInstance) { } const { oauthTokens, ...userData } = user; - - return { - ...userData, - connectedPlatforms: oauthTokens, - }; + return { ...userData, connectedPlatforms: oauthTokens }; }); - // ─── Logout ─── - app.post('/logout', async (request: FastifyRequest, reply: FastifyReply) => { reply.clearCookie('token', { path: '/' }); return { message: 'Logged out' }; }); } - -function generateState(): string { - return Math.random().toString(36).substring(2, 15); -} diff --git a/apps/backend/src/routes/cards.ts b/apps/backend/src/routes/cards.ts index f1af7b00..32fe835c 100644 --- a/apps/backend/src/routes/cards.ts +++ b/apps/backend/src/routes/cards.ts @@ -1,178 +1,138 @@ -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { handleDbError } from '../utils/error.util.js'; import { createCardSchema, updateCardSchema } from '../utils/validators.js'; +import * as cardService from '../services/cardService' + +import type { Card } from '@devcard/shared'; +import type { Prisma } from '@prisma/client'; +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; + + +interface CreateCardBody { + title: string; + linkIds: string[]; +} + +interface UpdateCardBody { + title?: string; + linkIds?: string[]; +} + +interface CardParams { + id: string; +} -export async function cardRoutes(app: FastifyInstance) { - app.addHook('preHandler', app.authenticate); +interface PlatformLink { + id: string; + userId: string; + platform: string; + username: string; + url: string; + displayOrder: number; + createdAt: Date; +} + +interface CardLinkWithPlatform { + id: string; + cardId: string; + platformLinkId: string; + displayOrder: number; + platformLink: PlatformLink; +} + +interface CardWithLinks { + id: string; + userId: string; + title: string; + isDefault: boolean; + createdAt: Date; + updatedAt: Date; + cardLinks: CardLinkWithPlatform[]; +} + +export async function cardRoutes(app: FastifyInstance): Promise { + app.addHook('preHandler', async (request, reply) => { + const server = request.server as any; + if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } + if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } + try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + }); // ─── List Cards ─── - app.get('/', async (request: FastifyRequest, reply: FastifyReply) => { - const userId = (request.user as any).id; - - const cards = await app.prisma.card.findMany({ - where: { userId }, - include: { - cardLinks: { - include: { platformLink: true }, - orderBy: { displayOrder: 'asc' }, - }, - }, - orderBy: { createdAt: 'asc' }, - }); - - return cards.map((card) => ({ - id: card.id, - title: card.title, - isDefault: card.isDefault, - links: card.cardLinks.map((cl) => cl.platformLink), - })); + app.get('/', async (request: FastifyRequest, reply: FastifyReply): Promise => { + const userId = (request.user as { id: string }).id; + try { + return await cardService.listCards(app, userId) + } catch (error) { + return handleDbError(error, request, reply) + } }); // ─── Create Card ─── - app.post('/', async (request: FastifyRequest, reply: FastifyReply) => { - const userId = (request.user as any).id; + app.post('/', async (request: FastifyRequest<{ Body: CreateCardBody }>, reply: FastifyReply): Promise => { + const userId = (request.user as { id: string }).id; const parsed = createCardSchema.safeParse(request.body); if (!parsed.success) { return reply.status(400).send({ error: 'Validation failed', details: parsed.error.flatten() }); } - // Check if user's first card → make it default - const cardCount = await app.prisma.card.count({ where: { userId } }); - - const card = await app.prisma.card.create({ - data: { - userId, - title: parsed.data.title, - isDefault: cardCount === 0, - cardLinks: { - create: parsed.data.linkIds.map((linkId, index) => ({ - platformLinkId: linkId, - displayOrder: index, - })), - }, - }, - include: { - cardLinks: { - include: { platformLink: true }, - orderBy: { displayOrder: 'asc' }, - }, - }, - }); - - return reply.status(201).send({ - id: card.id, - title: card.title, - isDefault: card.isDefault, - links: card.cardLinks.map((cl) => cl.platformLink), - }); + try { + const card = await cardService.createCard(app, userId, parsed.data) + return reply.status(201).send(card) + } catch (error: any) { + if (error?.code === 'OWNERSHIP') return reply.status(403).send({ error: 'One or more links do not belong to your account' }) + return handleDbError(error, request, reply) + } }); // ─── Update Card ─── - app.put('/:id', async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => { - const userId = (request.user as any).id; + app.put('/:id', async (request: FastifyRequest<{ Params: CardParams; Body: UpdateCardBody }>, reply: FastifyReply): Promise => { + const userId = (request.user as { id: string }).id; const { id } = request.params; - const existing = await app.prisma.card.findFirst({ - where: { id, userId }, - }); - - if (!existing) { - return reply.status(404).send({ error: 'Card not found' }); + try { + const parsed = updateCardSchema.safeParse(request.body) + if (!parsed.success) return reply.status(400).send({ error: 'Validation failed', details: parsed.error.flatten() }) + const updated = await cardService.updateCard(app, userId, id, parsed.data) + if (!updated) return reply.status(404).send({ error: 'Card not found' }) + return updated + } catch (error: any) { + if (error?.code === 'OWNERSHIP') return reply.status(403).send({ error: 'One or more links do not belong to your account' }) + return handleDbError(error, request, reply) } - - const parsed = updateCardSchema.safeParse(request.body); - if (!parsed.success) { - return reply.status(400).send({ error: 'Validation failed', details: parsed.error.flatten() }); - } - - // Update card title - if (parsed.data.title) { - await app.prisma.card.update({ - where: { id }, - data: { title: parsed.data.title }, - }); - } - - // Update card links if provided - if (parsed.data.linkIds) { - // Remove existing links - await app.prisma.cardLink.deleteMany({ where: { cardId: id } }); - // Add new links - await app.prisma.cardLink.createMany({ - data: parsed.data.linkIds.map((linkId, index) => ({ - cardId: id, - platformLinkId: linkId, - displayOrder: index, - })), - }); - } - - // Fetch updated card - const updated = await app.prisma.card.findUnique({ - where: { id }, - include: { - cardLinks: { - include: { platformLink: true }, - orderBy: { displayOrder: 'asc' }, - }, - }, - }); - - return { - id: updated!.id, - title: updated!.title, - isDefault: updated!.isDefault, - links: updated!.cardLinks.map((cl) => cl.platformLink), - }; }); // ─── Delete Card ─── - app.delete('/:id', async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => { - const userId = (request.user as any).id; + app.delete('/:id', async (request: FastifyRequest<{ Params: CardParams }>, reply: FastifyReply): Promise => { + const userId = (request.user as { id: string }).id; const { id } = request.params; - const existing = await app.prisma.card.findFirst({ - where: { id, userId }, - }); - - if (!existing) { - return reply.status(404).send({ error: 'Card not found' }); + try { + const res = await cardService.deleteCard(app, userId, id) + if (res && (res as any).code === 'NOT_FOUND') return reply.status(404).send({ error: 'Card not found' }) + if (res && (res as any).code === 'LAST_CARD') return reply.status(400).send({ error: 'Cannot delete the last remaining card. A user must have at least one card.' }) + return reply.status(204).send() + } catch (error) { + return handleDbError(error, request, reply) } - - await app.prisma.card.delete({ where: { id } }); - return reply.status(204).send(); }); // ─── Set Default Card ─── - app.put('/:id/default', async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => { - const userId = (request.user as any).id; + app.put('/:id/default', async (request: FastifyRequest<{ Params: CardParams }>, reply: FastifyReply): Promise => { + const userId = (request.user as { id: string }).id; const { id } = request.params; - const existing = await app.prisma.card.findFirst({ - where: { id, userId }, - }); - - if (!existing) { - return reply.status(404).send({ error: 'Card not found' }); + try { + const resp = await cardService.setDefaultCard(app, userId, id) + if (!resp) return reply.status(404).send({ error: 'Card not found' }) + return resp + } catch (error) { + return handleDbError(error, request, reply) } - - // Unset all other defaults - await app.prisma.card.updateMany({ - where: { userId }, - data: { isDefault: false }, - }); - - // Set this one - await app.prisma.card.update({ - where: { id }, - data: { isDefault: true }, - }); - - return { message: 'Default card updated' }; }); } diff --git a/apps/backend/src/routes/connect.ts b/apps/backend/src/routes/connect.ts index 952e8453..bb04194d 100644 --- a/apps/backend/src/routes/connect.ts +++ b/apps/backend/src/routes/connect.ts @@ -1,8 +1,17 @@ import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { randomBytes } from 'crypto'; +import { encrypt } from '../utils/encryption.js'; const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize'; const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'; +// Follow-capable tokens are stored under a dedicated platform key so that +// the authentication flow (read:user user:email scope, key = 'github') and +// the connect flow (user:follow scope, key = 'github_follow') never share +// the same OAuthToken record. Whichever flow runs last can no longer +// silently overwrite the other's access token. +const GITHUB_FOLLOW_PLATFORM = 'github_follow'; + interface OAuthCallbackQuery { code: string; state?: string; @@ -17,7 +26,12 @@ export async function connectRoutes(app: FastifyInstance) { // ─── Status ─── app.get('/status', { - preHandler: [app.authenticate], + preHandler: [async (request, reply) => { + const server = request.server as any; + if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } + if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } + try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + }], }, async (request: FastifyRequest, reply: FastifyReply) => { const userId = (request.user as any).id; @@ -32,20 +46,32 @@ export async function connectRoutes(app: FastifyInstance) { // ─── GitHub Connect ─── app.get('/github', { - preHandler: [app.authenticate], + preHandler: [async (request, reply) => { + const server = request.server as any; + if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } + if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } + try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + }], }, async (request: FastifyRequest, reply: FastifyReply) => { - // Generate a secure state token linking back to this user session - // In a real app, store this in Redis to cross-check in callback - const state = JSON.stringify({ - userId: (request.user as any).id, - nonce: generateState(), - }); + const userId = (request.user as any).id; + const nonce = generateState(); + + // Store nonce in Redis with 10-minute TTL. + // The callback verifies this to prevent CSRF attacks. + await app.redis.set( + `oauth:nonce:${nonce}`, + userId, + 'EX', + 600 + ); + + const state = JSON.stringify({ userId, nonce }); const redirectUri = `${process.env.BACKEND_URL}/api/connect/github/callback`; const params = new URLSearchParams({ client_id: process.env.GITHUB_CLIENT_ID || '', redirect_uri: redirectUri, - scope: 'user:follow', // ONLY asking for follow scope to avoid full profile access + scope: 'user:follow', state: Buffer.from(state).toString('base64'), }); @@ -61,17 +87,25 @@ export async function connectRoutes(app: FastifyInstance) { try { // Decode state to find which user requested the connect - const decodedState = parseGoogleState(state); + const decodedState = parseOAuthState(state); if (!decodedState) { return reply.redirect(`${process.env.PUBLIC_APP_URL}/settings?error=connect_failed`); } - const userId = decodedState.userId; - if (!userId) { + // Verify nonce was issued by this server -- prevents CSRF + const storedUserId = app.redis ? await app.redis.get(`oauth:nonce:${decodedState.nonce}`) : null; + + if (app.redis && (!storedUserId || storedUserId !== decodedState.userId)) { + app.log.warn({ nonce: decodedState.nonce }, 'OAuth CSRF check failed: nonce mismatch'); return reply.redirect(`${process.env.PUBLIC_APP_URL}/settings?error=invalid_state`); } + // Consume the nonce -- one-time use only (if redis configured) + if (app.redis) await app.redis.del(`oauth:nonce:${decodedState.nonce}`); + + const userId = decodedState.userId; + // Exchange code for token const tokenRes = await fetch(GITHUB_TOKEN_URL, { method: 'POST', @@ -94,14 +128,16 @@ export async function connectRoutes(app: FastifyInstance) { return reply.redirect(`${process.env.PUBLIC_APP_URL}/settings?error=connect_failed`); } - // Encrypt and store the token - const encryptedToken = app.encryption.encrypt(tokenData.access_token); + // Encrypt and store the token under the dedicated follow-scope key so + // that a subsequent login (which writes to 'github') cannot overwrite + // this follow-capable credential. + const encryptedToken = encrypt(tokenData.access_token); await app.prisma.oAuthToken.upsert({ where: { userId_platform: { userId, - platform: 'github', + platform: GITHUB_FOLLOW_PLATFORM, }, }, update: { @@ -110,7 +146,7 @@ export async function connectRoutes(app: FastifyInstance) { }, create: { userId, - platform: 'github', + platform: GITHUB_FOLLOW_PLATFORM, accessToken: encryptedToken, scopes: tokenData.scope || 'user:follow', }, @@ -124,8 +160,9 @@ export async function connectRoutes(app: FastifyInstance) { return reply.redirect(`${process.env.PUBLIC_APP_URL}/settings?connected=github`); - } catch (err) { - app.log.error('GitHub connect error:', err); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + app.log.error({ error, message }, 'GitHub connect error'); return reply.redirect(`${process.env.PUBLIC_APP_URL}/settings?error=server_error`); } }); @@ -134,11 +171,21 @@ export async function connectRoutes(app: FastifyInstance) { // ─── Disconnect ─── app.delete('/:platform', { - preHandler: [app.authenticate], + preHandler: [async (request, reply) => { + const server = request.server as any; + if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } + if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } + try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + }], }, async (request: FastifyRequest<{ Params: { platform: string } }>, reply: FastifyReply) => { const userId = (request.user as any).id; const { platform } = request.params; + const SUPPORTED_PLATFORMS = ['github', 'google', 'twitter', 'linkedin']; + if (!SUPPORTED_PLATFORMS.includes(platform)) { + return reply.status(400).send({ error: `Unsupported platform: ${platform}` }); + } + try { await app.prisma.oAuthToken.delete({ where: { @@ -149,13 +196,13 @@ export async function connectRoutes(app: FastifyInstance) { }, }); return { success: true }; - } catch (err) { + } catch (error) { return reply.status(404).send({ error: 'Connection not found' }); } }); } -function parseGoogleState(state: string): ParsedOAuthState | null { +function parseOAuthState(state: string): ParsedOAuthState | null { try { const decoded = JSON.parse(Buffer.from(state, 'base64').toString('utf-8')); @@ -170,5 +217,5 @@ function parseGoogleState(state: string): ParsedOAuthState | null { } function generateState(): string { - return Math.random().toString(36).substring(2, 15); + return randomBytes(32).toString('hex'); } diff --git a/apps/backend/src/routes/event.ts b/apps/backend/src/routes/event.ts new file mode 100644 index 00000000..4d4ee2d9 --- /dev/null +++ b/apps/backend/src/routes/event.ts @@ -0,0 +1,285 @@ +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { createEventSchema, joinEventSchema} from '../validations/event.validation'; + +import {generateUniqueSlug} from '../utils/slug' + + +type EventDetails = { + id: string; + name: string; + slug: string; + location: string; + description: string | null; + organizerUsername: string; + organizerDisplayName: string; + startDate: Date; + endDate: Date; + createdAt: Date; + attendeesCount: number +} + +type AttendeePublicProfile = { + id: string; + username: string; + displayName: string; + bio: string | null; + pronouns: string | null; + company: string | null; + avatarUrl: string | null; + accentColor: string; +} + + +type PaginatedAttendeesResponse = { + attendees: AttendeePublicProfile[]; + pagination: { + page: number; + limit: number; + total: number; + }; +} + +type EventWithAttendees = { + _count: { + attendees: number; + }; + attendees: { + user: { + id: string; + username: string; + displayName: string; + bio: string | null; + pronouns: string | null; + company: string | null; + avatarUrl: string | null; + accentColor: string; + }; + }[]; +} + +export async function eventRoutes(app:FastifyInstance) { + app.post('/', { preHandler: [async (request, reply) => { + const server = request.server as any; + if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } + if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } + try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + }] }, async (request: FastifyRequest<{ + Body: { + name: string, + description?: string, + startDate: string, + location: string, + endDate: string, + isPublic?: boolean + }}>, reply: FastifyReply) => { + const userId = (request.user as any).id; + const parsed = createEventSchema.safeParse(request.body); + if(!parsed.success){ + return reply.status(400).send({error: 'Bad request'}) + } + + const {name, description, startDate, endDate, isPublic ,location} = parsed.data + + let finalSlug = await generateUniqueSlug(name, async(slug) => { + const existing = await app.prisma.event.findUnique({where: {slug : slug}}) + + return !!existing + }) + + const startDateObj = new Date(startDate); + const endDateObj = new Date(endDate); + + try { + const newEvent = await app.prisma.event.create({ + data: { + name, + description, + slug: finalSlug, + location: location, + startDate: startDateObj, + endDate: endDateObj, + isPublic: isPublic ?? true, + organizerId: userId + } + }) + + return reply.status(201).send(newEvent); + } catch (error) { + app.log.error('Failed to create event'); + return reply.status(500).send({error: 'Failed to create event'}) + } + + }) + + //Returns event details and attendees count + app.get('/:slug', async(request: FastifyRequest<{Params: {slug: string}}>, reply: FastifyReply) => { + const paramsSlug = request.params.slug; + const details = await app.prisma.event.findUnique({ + where: { + slug: paramsSlug, + }, + include: { + _count: { + select: { + attendees: true + } + }, + organizer: { + select: { + username: true, + displayName: true + } + } + } + }) + if(!details){ + return reply.status(404).send({error: 'Event not found'}) + } + + const response: EventDetails = { + id: details.id, + name: details.name, + slug: details.slug, + description: details.description, + location: details.location, + organizerUsername: details.organizer.username, + organizerDisplayName: details.organizer.displayName, + startDate: details.startDate, + endDate: details.endDate, + createdAt: details.createdAt, + attendeesCount: details._count.attendees + } + + return response; + }) + + app.post('/:slug/join', { preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string}}>, reply: FastifyReply) => { + const userId = (request.user as any).id; + const paramsSlug = request.params.slug; + + const event = await app.prisma.event.findUnique({ + where: { + slug: paramsSlug + } + }) + + if(!event){ + return reply.status(404).send({error: 'Event not found'}) + } + + try { + await app.prisma.eventAttendee.create({ + data: { + eventId: event.id, + userId: userId, + joinedAt: new Date() + } + }) + + return reply.status(201).send({message: 'User joined successfully'}) + } catch (error:any) { + if(error.code === "P2002" ){ + return reply.status(409).send({error: 'Already joined'}) + } + app.log.error((error as Error).message); + return reply.status(500).send({error: 'Failed to join'}) + } + + }) + + app.delete('/:slug/leave', { preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string}}>, reply: FastifyReply) => { + const userId = (request.user as any).id; + const paramsSlug = request.params.slug; + + const event = await app.prisma.event.findUnique({ + where: { + slug: paramsSlug + } + }) + + if(!event){ + return reply.status(404).send({error: 'Event not found'}) + } + + try { + await app.prisma.eventAttendee.delete({ + where: { + userId_eventId: { + userId: userId, + eventId: event.id + } + } + }) + return reply.status(204).send({message: 'User left'}) + } catch (error:any) { + if(error.code === 'P2025'){ + return reply.status(404).send({error: 'User not found'}) + } + app.log.error((error as Error).message) + return reply.status(500).send({error: 'Failed to leave'}) + } + }) + + app.get('/:slug/attendees', async(request: FastifyRequest<{Params: {slug: string}, Querystring: {page?:string; limit?: string}}>, reply: FastifyReply) => { + const paramsSlug = request.params.slug; + const page = Math.max(1, Number(request.query.page) || 1); + const limit = Math.min(50, Number(request.query.limit) || 10); + const skip = (page - 1) * limit + const event = await app.prisma.event.findUnique({ + where: { + slug: paramsSlug + }, + include: { + _count: { + select: { attendees: true } + }, + attendees : { + include: { + user: { + select: { + id: true, + username: true, + displayName:true, + bio: true, + pronouns: true, + company: true, + avatarUrl: true, + accentColor: true + } + } + }, + skip, + take: limit, + orderBy: {joinedAt: 'desc'} + } + }, + })as EventWithAttendees | null; + + if(!event){ + return reply.status(404).send({error: 'Event not found'}) + } + + + const attendees = event.attendees.map((attendee: EventWithAttendees['attendees'][number]) => ({ + id: attendee.user.id, + username: attendee.user.username, + displayName: attendee.user.displayName, + bio: attendee.user.bio, + pronouns: attendee.user.pronouns, + company: attendee.user.company, + avatarUrl: attendee.user.avatarUrl, + accentColor: attendee.user.accentColor, + })); + + const response: PaginatedAttendeesResponse = { + attendees, + pagination: { + page, + limit, + total : event._count.attendees, + } + } + + return response; + }) +} \ No newline at end of file diff --git a/apps/backend/src/routes/follow.ts b/apps/backend/src/routes/follow.ts index aabc85b6..a152fc55 100644 --- a/apps/backend/src/routes/follow.ts +++ b/apps/backend/src/routes/follow.ts @@ -1,8 +1,16 @@ import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; import { decrypt } from '../utils/encryption.js'; +import { getErrorMessage } from '../utils/error.util.js'; +import { getPlatform, getProfileUrl, getWebViewUrl } from '@devcard/shared'; +import { followLogSchema } from '../validations/follow.validation.js'; export async function followRoutes(app: FastifyInstance) { - app.addHook('preHandler', app.authenticate); + app.addHook('preHandler', async (request, reply) => { + const server = request.server as any; + if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } + if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } + try { const payload = await request.jwtVerify(); if (payload) (request as any).user = payload; } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + }); // ─── Follow via API (Layer 1) ─── // Currently supports: GitHub @@ -14,13 +22,29 @@ export async function followRoutes(app: FastifyInstance) { const userId = (request.user as any).id; const { platform, targetUsername } = request.params; - // Get stored OAuth token for this platform + // GitHub follow tokens are stored under 'github_follow' to prevent the + // authentication flow (which writes to 'github') from silently overwriting + // the follow-capable credential. All other platforms use their plain name. + const tokenPlatform = platform === 'github' ? 'github_follow' : platform; + + // Get stored OAuth token for this platform (do this up-front so tests + // that inspect DB calls see the lookup regardless of follow strategy). const oauthToken = await app.prisma.oAuthToken.findUnique({ where: { - userId_platform: { userId, platform }, + userId_platform: { userId, platform: tokenPlatform }, }, }); + // Use WebView follow strategy if configured for the platform (e.g. LinkedIn, Twitter/X) + const platformDef = getPlatform(platform); + if (platformDef?.followStrategy === 'webview') { + const url = getWebViewUrl(platform, targetUsername) || getProfileUrl(platform, targetUsername); + return reply.send({ + strategy: 'webview', + url, + }); + } + if (!oauthToken) { return reply.status(400).send({ error: `Not connected to ${platform}. Please connect your ${platform} account first.`, @@ -33,9 +57,12 @@ export async function followRoutes(app: FastifyInstance) { try { let result; + let succeeded = false; + switch (platform) { case 'github': result = await followGitHub(accessToken, targetUsername, reply); + succeeded = result.success === true; break; default: return reply.status(400).send({ @@ -43,8 +70,8 @@ export async function followRoutes(app: FastifyInstance) { }); } - // If follow succeeded (or was handled by the function without throwing), log it - if (reply.statusCode === 200 || reply.statusCode === 204) { + // Log only genuine successes — not based on reply.statusCode default + if (succeeded) { app.prisma.followLog.create({ data: { followerId: userId, @@ -53,12 +80,12 @@ export async function followRoutes(app: FastifyInstance) { status: 'success', layer: 'api', }, - }).catch(err => app.log.error('Failed to log follow:', err)); + }).catch((err: unknown) => app.log.error(`Failed to log follow: ${getErrorMessage(err)}`)); } - return result; - } catch (err: any) { - app.log.error(`Follow error for ${platform}:`, err); + return result.response; + } catch (err: unknown) { + app.log.error(`Follow error for ${platform}: ${getErrorMessage(err)}`); app.prisma.followLog.create({ data: { @@ -68,11 +95,72 @@ export async function followRoutes(app: FastifyInstance) { status: 'error', layer: 'api', }, - }).catch(e => app.log.error('Failed to log follow error:', e)); + }).catch((e: unknown) => app.log.error(`Failed to log follow error: ${getErrorMessage(e)}`)); - return reply.status(500).send({ error: 'Follow action failed', message: err.message }); + return reply.status(500).send({ + error: 'Follow action failed', + message: getErrorMessage(err), + }); } }); + + // Log follow/connect event for Layer 2/3/4 strategies (WebView, deep-link, etc.) + // + // status and layer are analytics-impacting fields: they drive totalFollows counters + // and the follower-state dashboard. Both are validated against a strict allowlist + // before any database write — arbitrary client values are rejected with 400. + app.post('/:platform/:targetUsername/log', async ( + request: FastifyRequest<{ + Params: { platform: string; targetUsername: string }; + Body: { status?: string; layer?: string }; + }>, + reply: FastifyReply + ) => { + const userId = (request.user as any).id; + const { platform, targetUsername } = request.params; + + const parsed = followLogSchema.safeParse(request.body); + if (!parsed.success) { + return reply.status(400).send({ error: 'Invalid follow log payload' }); + } + + const { status, layer } = parsed.data; + + try { + const log = await app.prisma.followLog.create({ + data: { + followerId: userId, + targetUsername, + platform, + status, + layer, + }, + }); + return reply.send({ status: 'success', logId: log.id }); + } catch (error: any) { + app.log.error('Failed to log follow:', error); + return reply.status(500).send({ error: 'Failed to log follow event' }); + } + }); + + // ─── Clear follow log (reset Done state) ─── + app.delete('/:platform/:targetUsername/log', async ( + request: FastifyRequest<{ Params: { platform: string; targetUsername: string } }>, + reply: FastifyReply + ) => { + const userId = (request.user as any).id; + const { platform, targetUsername } = request.params; + + await app.prisma.followLog.deleteMany({ + where: { + followerId: userId, + platform, + targetUsername, + }, + }); + + return reply.send({ status: 'cleared' }); + }); } // ─── GitHub Follow (Layer 1) ─── @@ -81,7 +169,7 @@ async function followGitHub( accessToken: string, targetUsername: string, reply: FastifyReply -) { +): Promise<{ success: boolean; response: FastifyReply }> { const response = await fetch(`https://api.github.com/user/following/${targetUsername}`, { method: 'PUT', headers: { @@ -92,30 +180,42 @@ async function followGitHub( }); if (response.status === 204) { - return reply.send({ - status: 'success', - platform: 'github', - targetUsername, - message: `Now following ${targetUsername} on GitHub`, - }); + return { + success: true, + response: reply.send({ + status: 'success', + platform: 'github', + targetUsername, + message: `Now following ${targetUsername} on GitHub`, + }), + }; } if (response.status === 401 || response.status === 403) { - return reply.status(401).send({ - error: 'GitHub token expired or insufficient permissions', - requiresAuth: true, - }); + return { + success: false, + response: reply.status(401).send({ + error: 'GitHub token expired or insufficient permissions', + requiresAuth: true, + }), + }; } if (response.status === 404) { - return reply.status(404).send({ - error: `GitHub user '${targetUsername}' not found`, - }); + return { + success: false, + response: reply.status(404).send({ + error: `GitHub user '${targetUsername}' not found`, + }), + }; } const errorBody = await response.text(); - return reply.status(response.status).send({ - error: 'GitHub follow failed', - details: errorBody, - }); -} + return { + success: false, + response: reply.status(response.status).send({ + error: 'GitHub follow failed', + details: errorBody, + }), + }; +} \ No newline at end of file diff --git a/apps/backend/src/routes/nfc.ts b/apps/backend/src/routes/nfc.ts new file mode 100644 index 00000000..5cf13f0c --- /dev/null +++ b/apps/backend/src/routes/nfc.ts @@ -0,0 +1,114 @@ +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { z } from 'zod'; + +type NfcPayloadResponse = { + type: 'URI'; + payload: string; +}; + +const nfcQuerySchema = z.object({ + card: z.string().uuid('Invalid card ID format').optional(), +}); + +export async function nfcRoutes(app: FastifyInstance) { + app.addHook('preHandler', async (request, reply) => { + const server = request.server as any; + if (typeof server?.authenticate === 'function') { + await server.authenticate(request, reply); + return; + } + if (typeof (app as any).authenticate === 'function') { + await (app as any).authenticate(request, reply); + return; + } + try { + await request.jwtVerify(); + } catch (e) { + reply.status(401).send({ error: 'Unauthorized' }); + } + }); + + // GET /api/nfc/payload — returns NDEF URI payload for user's default DevCard URL + // GET /api/nfc/payload?card= — returns payload for a specific card + app.get( + '/payload', + async ( + request: FastifyRequest<{ Querystring: { card?: string } }>, + reply: FastifyReply + ) => { + const userId = (request.user as any).id; + + // Validate query params with Zod + const parseResult = nfcQuerySchema.safeParse(request.query); + if (!parseResult.success) { + return reply.status(400).send({ + error: 'Invalid query parameters', + details: parseResult.error.flatten(), + }); + } + + const { card: cardId } = parseResult.data; + + let username: string; + + // Fetch username + try { + const user = await app.prisma.user.findUnique({ + where: { id: userId }, + select: { username: true }, + }); + + if (!user) { + return reply.status(404).send({ + error: 'User not found', + }); + } + + username = user.username; + } catch (error) { + request.log.error( + { error }, + 'Failed to fetch user for NFC payload' + ); + return reply.status(500).send({ + error: 'Failed to fetch user profile', + }); + } + + // If a specific card is requested, verify ownership + if (cardId) { + try { + const card = await app.prisma.card.findUnique({ + where: { id: cardId }, + select: { userId: true }, + }); + + if (!card || card.userId !== userId) { + return reply.status(404).send({ + error: 'Card not found', + }); + } + } catch (error) { + request.log.error( + { error }, + 'Failed to fetch card for NFC payload' + ); + return reply.status(500).send({ + error: 'Failed to fetch card', + }); + } + } + +const safeUsername = encodeURIComponent(username); +const payloadUrl = `${process.env.PUBLIC_APP_URL}/${safeUsername}${ + cardId ? `?card=${encodeURIComponent(cardId)}` : '' +}`; + const response: NfcPayloadResponse = { + type: 'URI', + payload: payloadUrl, + }; + + return reply.send(response); + } + ); +} \ No newline at end of file diff --git a/apps/backend/src/routes/profiles.ts b/apps/backend/src/routes/profiles.ts index 99aacb8e..81026c74 100644 --- a/apps/backend/src/routes/profiles.ts +++ b/apps/backend/src/routes/profiles.ts @@ -1,43 +1,52 @@ import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; import { getProfileUrl } from '@devcard/shared'; -import { - updateProfileSchema, - createLinkSchema, - reorderLinksSchema, -} from '../utils/validators.js'; +import { updateProfileSchema, createLinkSchema, reorderLinksSchema } from '../utils/validators.js'; +import { getErrorMessage } from '../utils/error.util.js'; +import * as profileService from '../services/profileService' + +// ── Response types ──────────────────────────────────────────────────────────── +// Declared explicitly so the API contract is visible without tracing through +// Prisma's generic return types. Follows the convention in public.ts. + +type ProfileUpdateResponse = { + id: string; + email: string; + username: string; + displayName: string; + bio: string | null; + pronouns: string | null; + role: string | null; + company: string | null; + avatarUrl: string | null; + accentColor: string; +}; export async function profileRoutes(app: FastifyInstance) { // All profile routes require auth - app.addHook('preHandler', app.authenticate); + app.addHook('preHandler', async (request, reply) => { + const server = request.server as any; + if (typeof server?.authenticate === 'function') { + await server.authenticate(request, reply); + return; + } + if (typeof (app as any).authenticate === 'function') { + await (app as any).authenticate(request, reply); + return; + } + try { + await request.jwtVerify(); + } catch (e) { + reply.status(401).send({ error: 'Unauthorized' }); + } + }); // ─── Get Own Profile ─── app.get('/me', async (request: FastifyRequest, reply: FastifyReply) => { const userId = (request.user as any).id; - - const user = await app.prisma.user.findUnique({ - where: { id: userId }, - include: { - platformLinks: { - orderBy: { displayOrder: 'asc' }, - }, - cards: { - where: { isDefault: true }, - select: { id: true }, - take: 1, - }, - }, - }); - - if (!user) { - return reply.status(404).send({ error: 'User not found' }); - } - - const { provider, providerId, ...profileData } = user; - return { - ...profileData, - defaultCardId: user.cards[0]?.id || null, - }; + const user = await profileService.getOwnProfile(app, userId) + if (!user) return reply.status(404).send({ error: 'User not found' }) + return user }); // ─── Update Profile ─── @@ -50,9 +59,11 @@ export async function profileRoutes(app: FastifyInstance) { return reply.status(400).send({ error: 'Validation failed', details: parsed.error.flatten() }); } - // Check username uniqueness if changing - // Note: For production, consider adding a timestamp/version field to handle - // race conditions where two users might try to claim the same username simultaneously. + // Fast-path uniqueness check. This read-before-write eliminates the common + // case (clearly taken username) without touching the write path, but it + // cannot prevent the race window between two concurrent requests that both + // pass this check simultaneously. The unique constraint on the DB is the + // authoritative guard — P2002 below is the definitive conflict signal. if (parsed.data.username) { const existing = await app.prisma.user.findFirst({ where: { @@ -65,24 +76,14 @@ export async function profileRoutes(app: FastifyInstance) { } } - const updated = await app.prisma.user.update({ - where: { id: userId }, - data: parsed.data, - select: { - id: true, - email: true, - username: true, - displayName: true, - bio: true, - pronouns: true, - role: true, - company: true, - avatarUrl: true, - accentColor: true, - }, - }); - - return updated; + try { + const response = await profileService.updateProfile(app, userId, parsed.data) + return response + } catch (err: any) { + if (err?.code === 'P2002') return reply.status(409).send({ error: 'Username already taken' }) + app.log.error({ err }, 'DB error in PUT /profiles/me') + return reply.status(500).send({ error: 'Internal server error' }) + } }); // ─── Add Platform Link ─── @@ -95,26 +96,13 @@ export async function profileRoutes(app: FastifyInstance) { return reply.status(400).send({ error: 'Validation failed', details: parsed.error.flatten() }); } - // Auto-generate URL from platform registry if not provided - const url = parsed.data.url || getProfileUrl(parsed.data.platform, parsed.data.username); - - // Get next display order - const maxOrder = await app.prisma.platformLink.aggregate({ - where: { userId }, - _max: { displayOrder: true }, - }); - - const link = await app.prisma.platformLink.create({ - data: { - userId, - platform: parsed.data.platform, - username: parsed.data.username, - url, - displayOrder: (maxOrder._max.displayOrder ?? -1) + 1, - }, - }); - - return reply.status(201).send(link); + try { + const link = await profileService.createPlatformLink(app, userId, parsed.data) + return reply.status(201).send(link) + } catch (err: any) { + app.log.error({ err }, 'Failed to create platform link') + return reply.status(500).send({ error: 'Internal server error' }) + } }); // ─── Update Platform Link ─── @@ -123,31 +111,16 @@ export async function profileRoutes(app: FastifyInstance) { const userId = (request.user as any).id; const { id } = request.params; - const existing = await app.prisma.platformLink.findFirst({ - where: { id, userId }, - }); - - if (!existing) { - return reply.status(404).send({ error: 'Link not found' }); - } - - const parsed = createLinkSchema.safeParse(request.body); - if (!parsed.success) { - return reply.status(400).send({ error: 'Validation failed', details: parsed.error.flatten() }); + const parsedReq = createLinkSchema.safeParse(request.body) + if (!parsedReq.success) return reply.status(400).send({ error: 'Validation failed', details: parsedReq.error.flatten() }) + try { + const updated = await profileService.updatePlatformLink(app, userId, id, parsedReq.data) + if (!updated) return reply.status(404).send({ error: 'Link not found' }) + return updated + } catch (err: any) { + app.log.error({ err }, 'Failed to update platform link') + return reply.status(500).send({ error: 'Internal server error' }) } - - const url = parsed.data.url || getProfileUrl(parsed.data.platform, parsed.data.username); - - const updated = await app.prisma.platformLink.update({ - where: { id }, - data: { - platform: parsed.data.platform, - username: parsed.data.username, - url, - }, - }); - - return updated; }); // ─── Delete Platform Link ─── @@ -156,37 +129,28 @@ export async function profileRoutes(app: FastifyInstance) { const userId = (request.user as any).id; const { id } = request.params; - const existing = await app.prisma.platformLink.findFirst({ - where: { id, userId }, - }); - - if (!existing) { - return reply.status(404).send({ error: 'Link not found' }); + try { + const deleted = await profileService.deletePlatformLink(app, userId, id) + if (!deleted) return reply.status(404).send({ error: 'Link not found' }) + return reply.status(204).send() + } catch (err: any) { + app.log.error({ err }, 'Failed to delete platform link') + return reply.status(500).send({ error: 'Internal server error' }) } - - await app.prisma.platformLink.delete({ where: { id } }); - return reply.status(204).send(); }); // ─── Reorder Links ─── app.put('/me/links/reorder', async (request: FastifyRequest, reply: FastifyReply) => { const userId = (request.user as any).id; - const parsed = reorderLinksSchema.safeParse(request.body); - - if (!parsed.success) { - return reply.status(400).send({ error: 'Validation failed', details: parsed.error.flatten() }); + const parsedReq = reorderLinksSchema.safeParse(request.body) + if (!parsedReq.success) return reply.status(400).send({ error: 'Validation failed', details: parsedReq.error.flatten() }) + try { + const resp = await profileService.reorderLinks(app, userId, parsedReq.data.links) + return resp + } catch (err: any) { + app.log.error({ err }, 'Failed to reorder links') + return reply.status(500).send({ error: 'Internal server error' }) } - - await app.prisma.$transaction( - parsed.data.links.map((link) => - app.prisma.platformLink.updateMany({ - where: { id: link.id, userId }, - data: { displayOrder: link.displayOrder }, - }) - ) - ); - - return { message: 'Links reordered' }; }); } diff --git a/apps/backend/src/routes/public.ts b/apps/backend/src/routes/public.ts index f60e6133..27f544d8 100644 --- a/apps/backend/src/routes/public.ts +++ b/apps/backend/src/routes/public.ts @@ -1,135 +1,134 @@ -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import type { FastifyContextConfig, FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; import { generateQRBuffer, generateQRSvg } from '../utils/qr.js'; +import type { PlatformLink } from '@devcard/shared'; +import { getErrorMessage } from '../utils/error.util.js'; +import * as publicService from '../services/publicService' + + +// ── QR size bounds ──────────────────────────────────────────────────────────── +// Enforced before any DB query or image allocation. Values outside this range +// are rejected with 400 so a single unauthenticated request cannot trigger an +// unbounded memory allocation in the QR rasteriser. +const MIN_QR_SIZE = 1; +const MAX_QR_SIZE = 2048; + +// ── Cache constants ─────────────────────────────────────────────────────────── +// Public profile cache TTL matches the Cache-Control max-age (5 minutes). +// The QR session JWT TTL is 10 minutes so an offline scan remains valid well +// beyond the HTTP cache window. +const PROFILE_CACHE_TTL = 300; // seconds (5 minutes) +const CACHE_CONTROL_HEADER = 'public, max-age=300, stale-while-revalidate=60'; type PublicProfileLink = { id: string; platform: string; - username: string; - url: string; - displayOrder: number; + username: string; + url: string; + displayOrder: number; + followed?: boolean; } -type UsernamePublicProfileResponse = { - username: string; +type UsernamePublicProfileResponse = { + username: string; displayName: string; - bio: string | null; - pronouns: string | null; - role: string | null; + bio: string | null; + pronouns: string | null; + role: string | null; company: string | null; - avatarUrl: string | null; + avatarUrl: string | null; accentColor: string; links: PublicProfileLink[] -} +} type PublicProfileCardLink = { id: string; platform: string; - username: string; - url: string; + username: string; + url: string; + followed?: boolean; } type CardPublicProfileResponse = { - id: string; - title: string; + id: string; + title: string; owner: { - username: string; - displayName: string; + username: string; + displayName: string; bio: string | null; avatarUrl: string | null; - accentColor: string; - }; + accentColor: string; + }; links: PublicProfileCardLink[] } type UsernameCardPublicProfileResponse = { - title: string; + title: string; owner: { - username: string; + username: string; displayName: string; - bio: string | null; - pronouns: string | null; - role: string | null; + bio: string | null; + pronouns: string | null; + role: string | null; company: string | null; - avatarUrl: string | null; + avatarUrl: string | null; accentColor: string; - }; + }; links: PublicProfileCardLink[] } +// Represents a CardLink record with the joined PlatformLink relation +interface CardLinkWithPlatform { + id: string; + displayOrder: number; + platformLink: PlatformLink; +} + +// ── Internal Redis cache shape ──────────────────────────────────────────────── +// Extends the public response with the owner's DB id so that background view +// tracking can still fire on cache-HIT requests without an extra DB read. +type CachedProfileEntry = UsernamePublicProfileResponse & { _userId: string }; export async function publicRoutes(app: FastifyInstance) { + // ─── Public Profile ─────────────────────────────────────────────────────── // ─── Public Profile ─── - /** - * GET /api/public/:username + /** + * GET /api/u/:username * Returns the public profile information for a user. - */ - app.get('/:username', async (request: FastifyRequest<{ Params: { username: string } }>, reply: FastifyReply) => { - const { username } = request.params; - - const user = await app.prisma.user.findUnique({ - where: { username }, - include: { - platformLinks: { - orderBy: { displayOrder: 'asc' }, - }, + */ + app.get('/:username', { + config: { + rateLimit: { + max: 100, + timeWindow: '1 minute', }, - }); - - if (!user) { - return reply.status(404).send({ error: 'User not found' }); - } + }, + }, async (request: FastifyRequest<{ Params: { username: string } }>, reply: FastifyReply) => { + const { username } = request.params; + const cacheKey = `profile:${username}`; - // Try to extract viewer from Authorization header (soft auth) - let viewerId = null; + // Try to extract viewer from Authorization header (soft auth). + let viewerId: string | null = null try { if (request.headers.authorization) { - const decoded = await request.jwtVerify() as any; - if (decoded?.id !== user.id) { - viewerId = decoded.id; // Only log if they aren't the owner - } + const decoded = (await request.jwtVerify()) as { id?: string } + viewerId = decoded?.id ?? null } else { - viewerId = null; // Unauthenticated viewer + viewerId = null } - } catch (e) { - // Ignored if invalid token - } - - // Don't track if the owner is viewing their own profile - if (viewerId !== user.id) { - // Background view tracking - app.prisma.cardView.create({ - data: { - ownerId: user.id, - cardId: null, // this is a profile view, not a card view - viewerId, - viewerIp: request.ip || null, - viewerAgent: request.headers['user-agent'] || null, - source: (request.query as any)?.source || 'link', - }, - }).catch(err => app.log.error('Failed to log view:', err)); + } catch { + // ignored } - const response: UsernamePublicProfileResponse = { - username: user.username, - displayName: user.displayName, - bio: user.bio, - pronouns: user.pronouns, - role: user.role, - company: user.company, - avatarUrl: user.avatarUrl, - accentColor: user.accentColor, - links: user.platformLinks.map((link) => ({ - id: link.id, - platform: link.platform, - username: link.username, - url: link.url, - displayOrder: link.displayOrder, - })), + try { + const result = await publicService.getPublicProfile(app, username, viewerId, request) + if (!result) return reply.status(404).send({ error: 'User not found' }) + reply.header('X-Cache', result.cached ? 'HIT' : 'MISS').header('Cache-Control', CACHE_CONTROL_HEADER) + return result.data + } catch (err: any) { + app.log.error({ err }, 'Failed to fetch public profile') + return reply.status(500).send({ error: 'Internal server error' }) } - - return response; - }); /** @@ -139,133 +138,121 @@ export async function publicRoutes(app: FastifyInstance) { */ // ─── Shared Card View (Direct) ─── - app.get('/card/:cardId', async (request: FastifyRequest<{ Params: { cardId: string } }>, reply: FastifyReply) => { + app.get('/card/:cardId', { + config: { + rateLimit: { + max: 100, + timeWindow: '1 minute' + } + } as FastifyContextConfig + }, async (request: FastifyRequest<{ Params: { cardId: string } }>, reply: FastifyReply) => { const { cardId } = request.params; - const card = await app.prisma.card.findUnique({ - where: { id: cardId }, - include: { - user: true, - cardLinks: { - include: { platformLink: true }, - orderBy: { displayOrder: 'asc' }, - }, - }, - }); - - if (!card) { - return reply.status(404).send({ error: 'Card not found' }); - } - - const response: CardPublicProfileResponse = { - id: card.id, - title: card.title, - owner: { - username: card.user.username, - displayName: card.user.displayName, - bio: card.user.bio, - avatarUrl: card.user.avatarUrl, - accentColor: card.user.accentColor, - }, - links: card.cardLinks.map((cl) => ({ - id: cl.platformLink.id, - platform: cl.platformLink.platform, - username: cl.platformLink.username, - url: cl.platformLink.url, - })), + try { + const card = await publicService.getCardById(app, cardId) + if (!card) return reply.status(404).send({ error: 'Card not found' }) + const response = { id: card.id, title: card.title, owner: { username: card.user.username, displayName: card.user.displayName, bio: card.user.bio, avatarUrl: card.user.avatarUrl, accentColor: card.user.accentColor }, links: card.cardLinks.map((cl: any) => ({ id: cl.platformLink.id, platform: cl.platformLink.platform, username: cl.platformLink.username, url: cl.platformLink.url })) } + return response + } catch (err: any) { + app.log.error({ err }, 'Failed to fetch shared card') + return reply.status(500).send({ error: 'Internal server error' }) } - - return response; - }); + // ─── Public Card View ───────────────────────────────────────────────────── // ─── Public Card View ─── /** - * GET /api/public/:username/card/:cardId + * GET /api/u/:username/card/:cardId * Returns full owner profile + specific card data. * Used when viewing a card through username + cardId (e.g. QR code scans). - */ - app.get('/:username/card/:cardId', async (request: FastifyRequest<{ Params: { username: string; cardId: string } }>, reply: FastifyReply) => { - const { username, cardId } = request.params; - - const user = await app.prisma.user.findUnique({ - where: { username }, - }); - - if (!user) { - return reply.status(404).send({ error: 'User not found' }); - } - - const card = await app.prisma.card.findFirst({ - where: { id: cardId, userId: user.id }, - include: { - cardLinks: { - include: { platformLink: true }, - orderBy: { displayOrder: 'asc' }, - }, + */ + app.get('/:username/card/:cardId', { + config: { + rateLimit: { + max: 100, + timeWindow: '1 minute', }, - }); - - if (!card) { - return reply.status(404).send({ error: 'Card not found' }); - } + }, + }, async (request: FastifyRequest<{ Params: { username: string; cardId: string } }>, reply: FastifyReply) => { + const { username, cardId } = request.params; - let viewerId = null; + let viewerId: string | null = null try { if (request.headers.authorization) { - const decoded = await request.jwtVerify() as any; - if (decoded?.id !== user.id) { - viewerId = decoded.id; - } + const decoded = (await request.jwtVerify()) as { id?: string } + viewerId = decoded?.id ?? null } - } catch (e) {} + } catch { + // ignored + } - if (viewerId !== user.id) { - app.prisma.cardView.create({ - data: { - ownerId: user.id, - cardId: card.id, - viewerId, - viewerIp: request.ip || null, - viewerAgent: request.headers['user-agent'] || null, - source: (request.query as any)?.source || 'qr', - }, - }).catch(err => app.log.error('Failed to log card view:', err)); + try { + const result = await publicService.getUserCard(app, username, cardId, viewerId, request) + if (result.notFound) return reply.status(404).send({ error: 'User or card not found' }) + return result.data + } catch (err: any) { + app.log.error({ err }, 'Failed to fetch user card') + return reply.status(500).send({ error: 'Internal server error' }) } + }); + // ─── QR Session ────────────────────────────────────────────────────────── + // Returns a short-lived signed JWT encoding the public profile snapshot. + // Intended for native apps to generate QR codes that remain scannable when + // the device has no live network connectivity (offline QR mode, spec §5.9). + app.get('/:username/qr-session', { + config: { + rateLimit: { + max: 30, + timeWindow: '1 minute' + } + } as FastifyContextConfig + }, async (request: FastifyRequest<{ Params: { username: string } }>, reply: FastifyReply) => { + const { username } = request.params; + const cacheKey = `profile:${username}`; - const response: UsernameCardPublicProfileResponse = { - title: card.title, - owner: { - username: user.username, - displayName: user.displayName, - bio: user.bio, - pronouns: user.pronouns, - role: user.role, - company: user.company, - avatarUrl: user.avatarUrl, - accentColor: user.accentColor, - }, - links: card.cardLinks.map((cl) => ({ - id: cl.platformLink.id, - platform: cl.platformLink.platform, - username: cl.platformLink.username, - url: cl.platformLink.url, - displayOrder: cl.displayOrder, - })), + try { + const result = await publicService.getPublicProfile(app, username, null, request) + if (!result) return reply.status(404).send({ error: 'User not found' }) + const snapshot = result.data + const expiresIn = 600 + const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString() + const token = app.jwt.sign({ profile: snapshot, sub: username }, { expiresIn: '10m' }) + reply.header('Cache-Control', CACHE_CONTROL_HEADER) + return { token, tokenType: 'JWT', expiresIn, expiresAt } + } catch (err: any) { + app.log.error({ err }, 'Failed to create qr-session') + return reply.status(500).send({ error: 'Internal server error' }) } - return response; }); - // ─── QR Code Generation ─── + // ─── QR Code Generation ─────────────────────────────────────────────────── - app.get('/:username/qr', async (request: FastifyRequest<{ + app.get('/:username/qr', { + config: { + rateLimit: { + max: 50, // Lower limit for QR generation as it's more resource intensive + timeWindow: '1 minute' + } + } as FastifyContextConfig + }, async (request: FastifyRequest<{ Params: { username: string }; Querystring: { format?: string; size?: string }; }>, reply: FastifyReply) => { const { username } = request.params; const format = (request.query as any).format || 'png'; - const size = parseInt((request.query as any).size || '400', 10); + + // Parse and validate size before touching the DB or allocating any buffers. + // parseInt safely handles non-numeric strings (returns NaN) and ignores any + // trailing fractional part, so '400.9' → 400 which is within bounds. + const rawSize = (request.query as any).size; + const size = rawSize !== undefined ? parseInt(rawSize, 10) : 400; + + if (!Number.isInteger(size) || size < MIN_QR_SIZE || size > MAX_QR_SIZE) { + return reply.status(400).send({ + error: `QR size must be an integer between ${MIN_QR_SIZE} and ${MAX_QR_SIZE}`, + }); + } // Verify user exists const user = await app.prisma.user.findUnique({ @@ -278,18 +265,16 @@ export async function publicRoutes(app: FastifyInstance) { const profileUrl = `${process.env.PUBLIC_APP_URL}/u/${username}`; - if (format === 'svg') { - const svg = await generateQRSvg(profileUrl, { width: size }); - return reply - .header('Content-Type', 'image/svg+xml') - .header('Content-Disposition', `inline; filename="devcard-${username}.svg"`) - .send(svg); + try { + if (format === 'svg') { + const svg = await generateQRSvg(profileUrl, { width: size }) + return reply.header('Content-Type', 'image/svg+xml').header('Content-Disposition', `inline; filename="devcard-${username}.svg"`).send(svg) + } + const png = await generateQRBuffer(profileUrl, { width: size }) + return reply.header('Content-Type', 'image/png').header('Content-Disposition', `inline; filename="devcard-${username}.png"`).send(png) + } catch (error) { + app.log.error({ error, username, size, format }, 'QR generation failed') + return reply.status(500).send({ error: 'QR code generation failed' }) } - - const png = await generateQRBuffer(profileUrl, { width: size }); - return reply - .header('Content-Type', 'image/png') - .header('Content-Disposition', `inline; filename="devcard-${username}.png"`) - .send(png); }); } diff --git a/apps/backend/src/routes/team.ts b/apps/backend/src/routes/team.ts new file mode 100644 index 00000000..af177e52 --- /dev/null +++ b/apps/backend/src/routes/team.ts @@ -0,0 +1,389 @@ +import {Prisma, TeamRole } from '@prisma/client'; +import QRCode from 'qrcode' + +import {generateUniqueSlug} from '../utils/slug' +import { createTeamScehma,inviteMembers,updateTeam } from '../validations/team.validation'; + +import type {PlatformLink, PublicProfile} from '@devcard/shared' +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; + +type TeamMember = PublicProfile & { + teamRole: TeamRole + joinedAt: Date; +} + +type TeamProfile = { + id: string; + name: string; + slug: string; + description: string | null; + ownerId: string; + avatarUrl: string | null; + createdAt: Date; + updatedAt: Date | null; + members: TeamMember[]; +} + +export async function teamRoutes(app:FastifyInstance){ + app.post('/', { preHandler: [async (request, reply) => { + const server = request.server as any; + if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } + if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } + try { const payload = await request.jwtVerify(); if (payload) (request as any).user = payload; } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + }] }, async(request:FastifyRequest<{ + Body: {name: string, description? : string, avatarUrl?: string } + }>, reply: FastifyReply) => { + const userId = (request.user as any).id; + const parsed = createTeamScehma.safeParse(request.body); + if(!parsed.success){ + return reply.status(400).send({error: 'Bad request'}) + }; + const {name , description , avatarUrl} = parsed.data; + + const finalSlug = await generateUniqueSlug(name, async(slug) => { + const existing = await app.prisma.team.findUnique({where: {slug }}) + + return !!existing + }) + + try { + const team = await app.prisma.$transaction(async (tx) => { + const team = await tx.team.create({ + data: { + name, + slug: finalSlug, + description, + avatarUrl, + ownerId: userId, + } + }) + + await tx.teamMember.create({ + data: { + teamId : team.id, + userId, + role: TeamRole.OWNER, + joinedAt: new Date(), + } + }) + return team + }) + return reply.status(201).send(team) + + }catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + switch (error.code) { + case 'P2002': + return reply.status(409).send({ + error: 'Team slug already exists' + }); + + case 'P2003': + return reply.status(400).send({ + error: 'Invalid organizer' + }); + } + } + app.log.error('Failed to create a team'); + return reply.status(500).send({ + error: 'Failed to create team' + }); + } + }) + + app.get('/:slug', async(request: FastifyRequest<{Params: {slug: string}}>, reply: FastifyReply) => { + const paramsSlug = request.params.slug; + + try { + const details = await app.prisma.team.findUnique( + { + where: {slug: paramsSlug}, + include: { + members: { + include: { + user: { + include: { + platformLinks: true + } + } + } + } + } + } + ) + + if(!details){ + return reply.status(404).send({error: 'Team not found'}) + } + + const members = details.members.map((tm): TeamMember => ({ + username: tm.user.username, + displayName: tm.user.displayName, + bio: tm.user.bio, + pronouns: tm.user.pronouns, + role: tm.user.role, + company: tm.user.company, + avatarUrl: tm.user.avatarUrl, + accentColor: tm.user.accentColor, + links: tm.user.platformLinks.map((pl: PlatformLink) => ({ + id: pl.id, + platform: pl.platform, + username: pl.username, + url: pl.url, + displayOrder: pl.displayOrder, + })), + teamRole: tm.role, + joinedAt: tm.joinedAt, + + })) + + const response: TeamProfile = { + id: details.id, + name: details.name, + slug: details.slug, + description: details.description, + avatarUrl: details.avatarUrl, + ownerId: details.ownerId, + createdAt: details?.createdAt, + updatedAt: details.updatedAt, + members + } + + return response; + } catch (error) { + app.log.error(error); + return reply.status(500).send('Database query failed') + } + + }) + + app.post('/:slug/members', { preHandler: [async (request, reply) => { + const server = request.server as any; + if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } + if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } + try { const payload = await request.jwtVerify(); if (payload) (request as any).user = payload; } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + }] }, async(request: FastifyRequest<{Params: {slug:string}, Body:{username:string}}>, reply: FastifyReply) => { + const paramsSlug = request.params.slug; + const userId = (request.user as any).id; + const parsed = inviteMembers.safeParse(request.body); + if(!parsed.success){ + return reply.status(400).send({error: 'Bad request'}) + }; + const {username} = parsed.data; + try { + const teamDetails = await app.prisma.team.findUnique( + {where: {slug: paramsSlug }, + include:{ + owner: true, + members: { + include: { + user: true + } + } + } + } + ) + if(!teamDetails){ + return reply.status(404).send('Team not found'); + } + //Check request user is owner + if(teamDetails?.ownerId !== userId){ + return reply.status(403).send('Forbidden') + } + + const alreadyMember = teamDetails.members.find((u) => u.user.username === username) + + //Check invited username is not a member and owner; + if(alreadyMember || teamDetails.owner.username === username){ + return reply.status(409).send('Conflict') + } + + const invitedUserDetails = await app.prisma.user.findUnique(( + {where: { + username + }})) + + if(!invitedUserDetails){ + return reply.status(404).send('User not found') + } + + await app.prisma.teamMember.create({ + data: { + teamId: teamDetails.id, + userId: invitedUserDetails.id, + role: TeamRole.MEMBER, + joinedAt: new Date() + } + }) + + return reply.status(201).send('User invited') + + } catch (error) { + app.log.error(error); + return reply.status(500).send('Database query failed') + } + }) + + app.delete('/:slug/members/:userId', { preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string, userId: string}}>, reply: FastifyReply) => { + const paramsSlug = request.params.slug + const paramsUserId = request.params.userId + const userID = (request.user as any).id; + const teamDetails = await app.prisma.team.findUnique( + {where: {slug: paramsSlug}, + include: { + members: { + include:{ + user: true + } + } + } + }) + + if(!teamDetails){ + return reply.status(404).send({error: 'Team not found'}) + } + + const isMember = teamDetails.members.find((m) => paramsUserId === m.user.id) + + if(!isMember){ + return reply.status(404).send({ + error: 'Member not found', + }); + } + + const isOwner = teamDetails.ownerId === userID; + const isSelfRemove = paramsUserId === userID; + + if (!isOwner && !isSelfRemove) { + return reply.status(403).send({ + error: 'Forbidden', + }); + } + + //TODO: Assign owner role to next person + if(paramsUserId === teamDetails.ownerId){ + return reply.status(403).send({ + error: 'Owner cannot leave team', + }); + } + + if(isOwner || isSelfRemove){ + try { + await app.prisma.teamMember.delete({ + where: { + userId_teamId: { + teamId: teamDetails.id, + userId: paramsUserId + } + } + }) + reply.status(200).send('Member removed') + } catch (error) { + app.log.error(error); + + return reply.status(500).send('DB query failed') + } + } + }) + + app.patch('/:slug',{ preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string},Body: {description?:string, name?:string, avatarUrl?:string}}>, reply: FastifyReply) => { + const userId = (request.user as any).id; + const paramsSlug = request.params.slug; + const parsed = updateTeam.safeParse(request.body); + if(!parsed.success){ + return reply.status(400).send({error: 'Bad request'}) + }; + + const {name, description,avatarUrl} = parsed.data; + + + const teamDetails = await app.prisma.team.findUnique({where:{slug: paramsSlug}}) + + if(!teamDetails){ + return reply.status(404).send('Team not found'); + } + + if(teamDetails.ownerId !== userId){ + return reply.status(403).send({ + error: 'Forbidden', + }); + } + + try { + const updatedTeam = await app.prisma.team.update({ + where: { + slug: paramsSlug + }, + data: { + name, + description, + avatarUrl, + } + }) + return reply.status(200).send(updatedTeam) + } catch (error) { + app.log.error(error); + return reply.status(500).send('DB query failed') + } + + }) + + app.delete('/:slug',{ preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request:FastifyRequest<{Params:{slug: string}}>, reply:FastifyReply) => { + const userId = (request.user as any).id; + const paramsSlug = request.params.slug; + + + const teamDetails = await app.prisma.team.findUnique({ + where:{ + slug: paramsSlug + } + }) + + if(!teamDetails){ + return reply.status(404).send('Team not found'); + } + + if(teamDetails.ownerId !== userId){ + return reply.status(403).send({ + error: 'Forbidden', + }); + } + + try { + await app.prisma.team.delete({ + where: { + slug: paramsSlug, + } + }) + + return reply.status(200).send('Team deleted') + } catch (error) { + app.log.error(error) + + return reply.status(500).send('DB query failed') + } + }) + + app.get('/:slug/qr',async(request:FastifyRequest<{Params:{slug:string}}>, reply: FastifyReply) => { + const paramsSlug = request.params.slug; + try { + const teamDetails = await app.prisma.team.findUnique({ + where: { + slug: paramsSlug + } + }) + + if(!teamDetails){ + return reply.status(404).send('Team not found'); + } + + const url = `https://devcard.dev/team/${teamDetails.slug}` + const qrImage = await QRCode.toBuffer(url) + return reply.type('image/png').send(qrImage) + } catch (error) { + app.log.error(error); + return reply.status(500).send("QR generation failed") + } + + }) +} \ No newline at end of file diff --git a/apps/backend/src/server.ts b/apps/backend/src/server.ts index aea785d9..d494cf94 100644 --- a/apps/backend/src/server.ts +++ b/apps/backend/src/server.ts @@ -10,8 +10,8 @@ async function start() { try { await app.listen({ port: PORT, host: HOST }); app.log.info(`🚀 DevCard API running at http://${HOST}:${PORT}`); - } catch (err) { - app.log.error(err); + } catch (error) { + app.log.error(error); process.exit(1); } } diff --git a/apps/backend/src/services/authService.ts b/apps/backend/src/services/authService.ts new file mode 100644 index 00000000..9af718c5 --- /dev/null +++ b/apps/backend/src/services/authService.ts @@ -0,0 +1,35 @@ +import { randomBytes } from 'crypto'; + +export function generateState(): string { + return randomBytes(32).toString('hex'); +} + +export function buildOAuthState(clientState: string, mobileRedirectUri: string): string { + if (!clientState) { + return generateState(); + } + + if (clientState.startsWith('mobile_') && mobileRedirectUri) { + const encodedRedirect = Buffer.from(mobileRedirectUri, 'utf8').toString('base64url'); + return `${clientState}.${encodedRedirect}.${generateState()}`; + } + + return `${clientState}.${generateState()}`; +} + +export function getMobileRedirectUri(state?: string): string | null { + if (!state?.startsWith('mobile_')) { + return null; + } + + const encodedRedirect = state.split('.')[1]; + if (!encodedRedirect) { + return null; + } + + try { + return Buffer.from(encodedRedirect, 'base64url').toString('utf8'); + } catch { + return null; + } +} diff --git a/apps/backend/src/services/cardService.ts b/apps/backend/src/services/cardService.ts new file mode 100644 index 00000000..a9721783 --- /dev/null +++ b/apps/backend/src/services/cardService.ts @@ -0,0 +1,93 @@ +import type { FastifyInstance } from 'fastify' +import type { Prisma } from '@prisma/client' + +export async function listCards(app: FastifyInstance, userId: string) { + const cards = await app.prisma.card.findMany({ + where: { userId }, + take: 50, + include: { cardLinks: { include: { platformLink: true }, orderBy: { displayOrder: 'asc' } } }, + orderBy: { createdAt: 'asc' }, + }) + + return cards.map((card: any) => ({ id: card.id, title: card.title, isDefault: card.isDefault, links: card.cardLinks.map((cl: any) => cl.platformLink) })) +} + +export async function createCard(app: FastifyInstance, userId: string, body: { title: string; linkIds: string[] }) { + if (body.linkIds.length > 0) { + const ownedLinks = await app.prisma.platformLink.findMany({ where: { id: { in: body.linkIds }, userId }, select: { id: true } }) + if (ownedLinks.length !== body.linkIds.length) throw Object.assign(new Error('Link ownership mismatch'), { code: 'OWNERSHIP' }) + } + + const cardCount = await app.prisma.card.count({ where: { userId } }) + + const card = await app.prisma.card.create({ + data: { + userId, + title: body.title, + isDefault: cardCount === 0, + cardLinks: { create: body.linkIds.map((linkId, index) => ({ platformLinkId: linkId, displayOrder: index })) }, + }, + include: { cardLinks: { include: { platformLink: true }, orderBy: { displayOrder: 'asc' } } }, + }) + + return { id: card.id, title: card.title, isDefault: card.isDefault, links: card.cardLinks.map((cl: any) => cl.platformLink) } +} + +export async function updateCard(app: FastifyInstance, userId: string, id: string, body: { title?: string; linkIds?: string[] }) { + const existing = await app.prisma.card.findFirst({ where: { id, userId } }) + if (!existing) return null + + if (body.title) { + await app.prisma.card.update({ where: { id }, data: { title: body.title } }) + } + + if (body.linkIds) { + if (body.linkIds.length > 0) { + const ownedLinks = await app.prisma.platformLink.findMany({ where: { id: { in: body.linkIds }, userId }, select: { id: true } }) + if (ownedLinks.length !== body.linkIds.length) throw Object.assign(new Error('Link ownership mismatch'), { code: 'OWNERSHIP' }) + } + + const linkIds = body.linkIds + await app.prisma.$transaction(async (tx: Prisma.TransactionClient) => { + await tx.cardLink.deleteMany({ where: { cardId: id } }) + if (linkIds.length > 0) { + await tx.cardLink.createMany({ data: linkIds.map((linkId, index) => ({ cardId: id, platformLinkId: linkId, displayOrder: index })) }) + } + }) + } + + const updated = await app.prisma.card.findUnique({ where: { id }, include: { cardLinks: { include: { platformLink: true }, orderBy: { displayOrder: 'asc' } } } }) + return { id: updated!.id, title: updated!.title, isDefault: updated!.isDefault, links: updated!.cardLinks.map((cl: any) => cl.platformLink) } +} + +export async function deleteCard(app: FastifyInstance, userId: string, id: string) { + return await app.prisma.$transaction(async (tx: Prisma.TransactionClient) => { + const existing = await tx.card.findFirst({ where: { id, userId } }) + if (!existing) return Object.assign(new Error('NotFound'), { code: 'NOT_FOUND' }) + + const userCardCount = await tx.card.count({ where: { userId } }) + if (userCardCount <= 1) return Object.assign(new Error('Cannot delete last card'), { code: 'LAST_CARD' }) + + if (existing.isDefault) { + const oldestRemainingCard = await tx.card.findFirst({ where: { userId, id: { not: id } }, orderBy: { createdAt: 'asc' } }) + if (oldestRemainingCard) { + await tx.card.update({ where: { id: oldestRemainingCard.id }, data: { isDefault: true } }) + } + } + + await tx.card.delete({ where: { id } }) + return null + }) +} + +export async function setDefaultCard(app: FastifyInstance, userId: string, id: string) { + const existing = await app.prisma.card.findFirst({ where: { id, userId } }) + if (!existing) return null + + await app.prisma.$transaction(async (tx: Prisma.TransactionClient) => { + await tx.card.updateMany({ where: { userId }, data: { isDefault: false } }) + await tx.card.update({ where: { id }, data: { isDefault: true } }) + }) + + return { message: 'Default card updated' } +} diff --git a/apps/backend/src/services/profileService.ts b/apps/backend/src/services/profileService.ts new file mode 100644 index 00000000..dc97b2a4 --- /dev/null +++ b/apps/backend/src/services/profileService.ts @@ -0,0 +1,74 @@ +import type { FastifyInstance } from 'fastify' +import { getProfileUrl } from '@devcard/shared' +import type { PlatformLink } from '@devcard/shared' +import { getErrorMessage } from '../utils/error.util.js' + +export async function getOwnProfile(app: FastifyInstance, userId: string) { + const user = await app.prisma.user.findUnique({ + where: { id: userId }, + include: { + platformLinks: { orderBy: { displayOrder: 'asc' } }, + cards: { where: { isDefault: true }, select: { id: true }, take: 1 }, + }, + }) + + if (!user) return null + + const { provider, providerId, ...profileData } = user as any + return { ...profileData, defaultCardId: user.cards[0]?.id || null } +} + +export async function updateProfile(app: FastifyInstance, userId: string, data: any) { + // Fast-path uniqueness check + if (data.username) { + const existing = await app.prisma.user.findFirst({ + where: { username: data.username, NOT: { id: userId } }, + }) + if (existing) throw Object.assign(new Error('Username taken'), { code: 'P2002' }) + } + + const currentUser = await app.prisma.user.findUnique({ where: { id: userId }, select: { username: true } }) + + try { + const response = await app.prisma.user.update({ where: { id: userId }, data, select: { + id: true, email: true, username: true, displayName: true, bio: true, pronouns: true, role: true, company: true, avatarUrl: true, accentColor: true + } }) + + if (app.redis && currentUser) { + app.redis.del(`profile:${currentUser.username}`).catch((err: unknown) => + app.log.warn(`Failed to invalidate profile cache: ${getErrorMessage(err)}`) + ) + } + + return response + } catch (err: any) { + if (err?.code === 'P2002') throw err + app.log.error({ err }, 'DB error in updateProfile') + throw err + } +} + +export async function createPlatformLink(app: FastifyInstance, userId: string, linkData: any) { + const url = linkData.url || getProfileUrl(linkData.platform, linkData.username) + const maxOrder = await app.prisma.platformLink.aggregate({ where: { userId }, _max: { displayOrder: true } }) + return app.prisma.platformLink.create({ data: { userId, platform: linkData.platform, username: linkData.username, url, displayOrder: (maxOrder._max.displayOrder ?? -1) + 1 } }) +} + +export async function updatePlatformLink(app: FastifyInstance, userId: string, id: string, linkData: any) { + const existing = await app.prisma.platformLink.findFirst({ where: { id, userId } }) + if (!existing) return null + const url = linkData.url || getProfileUrl(linkData.platform, linkData.username) + return app.prisma.platformLink.update({ where: { id }, data: { platform: linkData.platform, username: linkData.username, url } }) +} + +export async function deletePlatformLink(app: FastifyInstance, userId: string, id: string) { + const existing = await app.prisma.platformLink.findFirst({ where: { id, userId } }) + if (!existing) return false + await app.prisma.platformLink.delete({ where: { id } }) + return true +} + +export async function reorderLinks(app: FastifyInstance, userId: string, links: Array<{ id: string; displayOrder: number }>) { + await app.prisma.$transaction(links.map((link) => app.prisma.platformLink.updateMany({ where: { id: link.id, userId }, data: { displayOrder: link.displayOrder } }))) + return { message: 'Links reordered' } +} diff --git a/apps/backend/src/services/publicService.ts b/apps/backend/src/services/publicService.ts new file mode 100644 index 00000000..758ab78f --- /dev/null +++ b/apps/backend/src/services/publicService.ts @@ -0,0 +1,67 @@ +import type { FastifyInstance } from 'fastify' +import { getErrorMessage } from '../utils/error.util.js' + +const PROFILE_CACHE_TTL = 300 +const CACHE_CONTROL_HEADER = 'public, max-age=300, stale-while-revalidate=60' + +export async function getPublicProfile(app: FastifyInstance, username: string, viewerId: string | null, request: any) { + const cacheKey = `profile:${username}` + + if (app.redis) { + try { + const cached = await app.redis.get(cacheKey) + if (cached) { + const { _userId, ...profileData } = JSON.parse(cached) + if (viewerId && viewerId !== _userId) { + app.prisma.cardView.create({ data: { ownerId: _userId, cardId: null, viewerId, viewerIp: request.ip || null, viewerAgent: request.headers['user-agent'] || null, source: request.query?.source || 'link' } }).catch((err: unknown) => app.log.error(`Failed to log view: ${getErrorMessage(err)}`)) + } + return { cached: true, data: profileData, cacheKey } + } + } catch (err) { + app.log.warn(`Redis cache read failed for ${cacheKey}: ${getErrorMessage(err)}`) + } + } + + const user = await app.prisma.user.findUnique({ where: { username }, include: { platformLinks: { orderBy: { displayOrder: 'asc' } } } }) + if (!user) return null + + if (viewerId && viewerId !== user.id) { + app.prisma.cardView.create({ data: { ownerId: user.id, cardId: null, viewerId, viewerIp: request.ip || null, viewerAgent: request.headers['user-agent'] || null, source: request.query?.source || 'link' } }).catch((error: unknown) => app.log.error(`Failed to log view: ${getErrorMessage(error)}`)) + } + + let followedLinkIds: string[] = [] + if (viewerId && user.platformLinks.length > 0) { + const successfulFollows = await app.prisma.followLog.findMany({ where: { followerId: viewerId, status: 'success', OR: user.platformLinks.map((link: any) => ({ platform: link.platform, targetUsername: link.username })) }, select: { platform: true, targetUsername: true } }) + followedLinkIds = user.platformLinks.filter((link: any) => successfulFollows.some((f: any) => f.platform === link.platform && f.targetUsername.toLowerCase() === link.username.toLowerCase())).map((l: any) => l.id) + } + + const baseLinks = user.platformLinks.map((link: any) => ({ id: link.id, platform: link.platform, username: link.username, url: link.url, displayOrder: link.displayOrder, followed: false })) + + if (app.redis) { + const entry = { _userId: user.id, username: user.username, displayName: user.displayName, bio: user.bio, pronouns: user.pronouns, role: user.role, company: user.company, avatarUrl: user.avatarUrl, accentColor: user.accentColor, links: baseLinks } + app.redis.set(cacheKey, JSON.stringify(entry), 'EX', PROFILE_CACHE_TTL).catch((err: unknown) => app.log.warn(`Redis cache write failed for ${cacheKey}: ${getErrorMessage(err)}`)) + } + + const response = { username: user.username, displayName: user.displayName, bio: user.bio, pronouns: user.pronouns, role: user.role, company: user.company, avatarUrl: user.avatarUrl, accentColor: user.accentColor, links: baseLinks.map((link) => ({ ...link, followed: followedLinkIds.includes(link.id) })) } + + return { cached: false, data: response, cacheKey } +} + +export async function getCardById(app: FastifyInstance, cardId: string) { + const card = await app.prisma.card.findUnique({ where: { id: cardId }, include: { user: true, cardLinks: { include: { platformLink: true }, orderBy: { displayOrder: 'asc' } } } }) + return card +} + +export async function getUserCard(app: FastifyInstance, username: string, cardId: string, viewerId: string | null, request: any) { + const user = await app.prisma.user.findUnique({ where: { username } }) + if (!user) return { notFound: true } + const card = await app.prisma.card.findFirst({ where: { id: cardId, userId: user.id }, include: { cardLinks: { include: { platformLink: true }, orderBy: { displayOrder: 'asc' } } } }) + if (!card) return { notFound: true } + + if (viewerId && viewerId !== user.id) { + app.prisma.cardView.create({ data: { ownerId: user.id, cardId: card.id, viewerId, viewerIp: request.ip || null, viewerAgent: request.headers['user-agent'] || null, source: request.query?.source || 'qr' } }).catch((error: unknown) => app.log.error(`Failed to log view: ${getErrorMessage(error)}`)) + } + + const response = { title: card.title, owner: { username: user.username, displayName: user.displayName, bio: user.bio, pronouns: user.pronouns, role: user.role, company: user.company, avatarUrl: user.avatarUrl, accentColor: user.accentColor }, links: card.cardLinks.map((cl: any) => ({ id: cl.platformLink.id, platform: cl.platformLink.platform, username: cl.platformLink.username, url: cl.platformLink.url, displayOrder: cl.displayOrder })) } + return { notFound: false, data: response } +} diff --git a/apps/backend/src/types/fastify.d.ts b/apps/backend/src/types/fastify.d.ts new file mode 100644 index 00000000..8e7aee95 --- /dev/null +++ b/apps/backend/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +import '@fastify/cookie'; +import { FastifyRequest } from 'fastify'; + +declare module 'fastify' { + interface FastifyRequest { + cookies: Record; + } +} diff --git a/apps/backend/src/utils/error.util.ts b/apps/backend/src/utils/error.util.ts new file mode 100644 index 00000000..fef1b98b --- /dev/null +++ b/apps/backend/src/utils/error.util.ts @@ -0,0 +1,32 @@ +import type { FastifyReply, FastifyRequest } from 'fastify'; +import { Prisma } from '@prisma/client'; + +export function getErrorMessage(err: unknown): string { + return err instanceof Error ? err.message : String(err); +} + +export function handleDbError(error: unknown, request: FastifyRequest, reply: FastifyReply) { + request.log.error(error); + + if (error instanceof Prisma.PrismaClientKnownRequestError) { + // P2002: Unique constraint failed + if (error.code === 'P2002') { + return reply.status(409).send({ error: 'Conflict: Record already exists or violates unique constraint' }); + } + // P2025: Record to update not found + if (error.code === 'P2025') { + return reply.status(404).send({ error: 'Not Found: Record does not exist' }); + } + // P2003: Foreign key constraint failed + if (error.code === 'P2003') { + return reply.status(400).send({ error: 'Constraint failed: Related record not found or invalid' }); + } + return reply.status(400).send({ error: `Database error: ${error.message}` }); + } + + if (error instanceof Prisma.PrismaClientValidationError) { + return reply.status(400).send({ error: 'Database validation failed' }); + } + + return reply.status(500).send({ error: 'Internal Server Error' }); +} \ No newline at end of file diff --git a/apps/backend/src/utils/slug.ts b/apps/backend/src/utils/slug.ts new file mode 100644 index 00000000..24b772f3 --- /dev/null +++ b/apps/backend/src/utils/slug.ts @@ -0,0 +1,19 @@ +export function createSlug(name:string){ + return name.toLowerCase().trim().replace(/\s+/g, '-').replace(/[^a-z0-9-]+/g, '').replace(/-+/g, '-').replace(/^-+|-+$/g, '') +} + +export async function generateUniqueSlug(name: string, + slugExists: (slug: string) => Promise +){ + const cleanSlug = createSlug(name) + let finalSlug = cleanSlug; + while(true){ + const exists = await slugExists(finalSlug) + + if(!exists) break; + + const randomSuffix = Math.random().toString(36).substring(2,6); + finalSlug = `${cleanSlug}-${randomSuffix}` + } + return finalSlug; +} diff --git a/apps/backend/src/utils/validateEnv.ts b/apps/backend/src/utils/validateEnv.ts new file mode 100644 index 00000000..cd361fc8 --- /dev/null +++ b/apps/backend/src/utils/validateEnv.ts @@ -0,0 +1,76 @@ +/** + * Startup environment validation. + * + * Validates all required secrets before the application registers any plugins. + * Missing or insecure values cause an immediate, deterministic process exit so + * the server never reaches a partially-initialised auth state. + * + * Call this at the very top of buildApp(), before any Fastify plugin registration. + */ + +/** + * Secrets that are committed to the public repository and must not be used in + * production. Any match triggers an immediate startup failure. + */ +const KNOWN_INSECURE_DEFAULTS: ReadonlySet = new Set([ + 'dev-secret-change-me', +]); + +/** + * Validates that all required secrets are present and safe. + * Exits the process with code 1 on any violation, logging all failures at once + * so operators can fix everything in a single deploy cycle. + * + * Secrets are never logged — only their presence and safety are reported. + */ +export function validateEnv(): void { + const errors: string[] = []; + const isProduction = process.env.NODE_ENV === 'production'; + + // ── JWT_SECRET ────────────────────────────────────────────────────────────── + const jwtSecret = process.env.JWT_SECRET; + + if (!jwtSecret) { + errors.push( + 'JWT_SECRET is not set. Generate a secure value with:\n' + + ' node -e "console.log(require(\'crypto\').randomBytes(64).toString(\'hex\'))"', + ); + } else if (isProduction && KNOWN_INSECURE_DEFAULTS.has(jwtSecret)) { + errors.push( + 'JWT_SECRET is set to a known insecure default and cannot be used in production.\n' + + ' Generate a secure value with:\n' + + ' node -e "console.log(require(\'crypto\').randomBytes(64).toString(\'hex\'))"', + ); + } + + // ── ENCRYPTION_KEY ────────────────────────────────────────────────────────── + // getEncryptionKey() in utils/encryption.ts already throws at call-time when + // this is missing, but catching it at startup is safer — the error surfaces + // before any request is served rather than mid-flight on the first encrypt/ + // decrypt call. + const encryptionKey = process.env.ENCRYPTION_KEY; + + if (!encryptionKey) { + errors.push( + 'ENCRYPTION_KEY is not set. Generate a secure value with:\n' + + ' node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"', + ); + } + + // ── Fail fast ─────────────────────────────────────────────────────────────── + if (errors.length === 0) { + return; + } + + console.error(''); + console.error('╔══════════════════════════════════════════════════════════╗'); + console.error('║ STARTUP FAILED — missing or insecure required secrets ║'); + console.error('╚══════════════════════════════════════════════════════════╝'); + console.error(''); + for (const msg of errors) { + console.error(` ✖ ${msg}`); + console.error(''); + } + + process.exit(1); +} diff --git a/apps/backend/src/utils/validators.ts b/apps/backend/src/utils/validators.ts index 80d2caa1..bd41bef2 100644 --- a/apps/backend/src/utils/validators.ts +++ b/apps/backend/src/utils/validators.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { getPlatform } from '@devcard/shared'; export const updateProfileSchema = z.object({ displayName: z.string().min(1).max(100).optional(), @@ -22,6 +23,17 @@ export const createLinkSchema = z.object({ platform: z.string().min(1), username: z.string().min(1).max(200), url: z.string().url().optional(), +}).superRefine((data, ctx) => { + const platformDef = getPlatform(data.platform); + if (platformDef?.validationRegex) { + if (!platformDef.validationRegex.test(data.username)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Invalid format for ${platformDef.name} handle`, + path: ['username'], + }); + } + } }); export const reorderLinksSchema = z.object({ diff --git a/apps/backend/src/validations/event.validation.ts b/apps/backend/src/validations/event.validation.ts new file mode 100644 index 00000000..0fc4044f --- /dev/null +++ b/apps/backend/src/validations/event.validation.ts @@ -0,0 +1,12 @@ +import {z} from 'zod' + +export const createEventSchema = z.object({ + name: z.string().min(3, 'Event name must be at least 3 characters long').max(100,'Event name cannot be longer than 100 characters'), + description: z.string().min(1).optional(), + location: z.string().min(2, 'Location should be atleast 2 characters long').max(100,'Location cannot be longer than 100 characters'), + startDate: z.string().pipe(z.coerce.date()), + endDate: z.string().pipe(z.coerce.date()), + isPublic: z.boolean().default(true) +}) + +export const joinEventSchema = z.object({}) \ No newline at end of file diff --git a/apps/backend/src/validations/follow.validation.ts b/apps/backend/src/validations/follow.validation.ts new file mode 100644 index 00000000..319f1de1 --- /dev/null +++ b/apps/backend/src/validations/follow.validation.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; + +/** + * Strict allowlist schema for analytics-impacting follow log fields. + * + * Both `status` and `layer` feed directly into analytics counters and the + * follower-state dashboard. Only the values enumerated below may be + * persisted — all other values are rejected before any database write. + * + * status: + * 'success' — the follow action completed and was accepted by the platform + * 'failed' — the action completed but was rejected (e.g. rate-limit, block) + * 'pending' — the action was initiated; outcome not yet confirmed by client + * + * layer (hybrid follow engine interaction surface): + * 'foreground' — user interacted directly with an in-app WebView session + * 'background' — follow triggered through a passive deep-link / redirect strategy + */ +export const followLogSchema = z.object({ + status: z.enum(['success', 'failed', 'pending'], { + errorMap: () => ({ + message: "status must be one of: 'success', 'failed', 'pending'", + }), + }), + layer: z.enum(['foreground', 'background'], { + errorMap: () => ({ + message: "layer must be one of: 'foreground', 'background'", + }), + }), +}); + +export type FollowLogBody = z.infer; diff --git a/apps/backend/src/validations/team.validation.ts b/apps/backend/src/validations/team.validation.ts new file mode 100644 index 00000000..153333c0 --- /dev/null +++ b/apps/backend/src/validations/team.validation.ts @@ -0,0 +1,26 @@ +import {z} from 'zod'; + +export const createTeamScehma = z.object({ + name: z.string().min(3, 'Event name must be at least 3 characters long').max(100,'Event name cannot be longer than 100 characters'), + description: z.string().min(1).optional(), + avatarUrl : z.string().url().optional(), +}) + + +export const inviteMembers = z.object({ + username: z.string().min(1,'Username must be atleast 1 character') +}) + +export const updateTeam = z.object({ + name: z.string().min(1, 'Name must be at least 1 character').optional(), + description: z.string().min(1,'Description must be at least 1 character').optional(), + avatarUrl: z.string().url('Invalid avatar URL').optional(), +}).refine( + (data) => + data.name !== undefined || + data.description !== undefined || + data.avatarUrl !== undefined, + { + message: 'At least one field is required', + } +) \ No newline at end of file diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx index 811892f4..d577bd7a 100644 --- a/apps/mobile/App.tsx +++ b/apps/mobile/App.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { NavigationContainer } from '@react-navigation/native'; +import { NavigationContainer, LinkingOptions } from '@react-navigation/native'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; @@ -7,21 +7,44 @@ import { AuthProvider, useAuth } from './src/context/AuthContext'; import { ThemeProvider } from './src/context/ThemeContext'; import AuthStack from './src/navigation/AuthStack'; import MainTabs from './src/navigation/MainTabs'; +import SplashScreen from './src/screens/SplashScreen'; +import { DEEP_LINK_SCHEME } from './src/config'; import { Linking, StyleSheet } from 'react-native'; +// ── Deep Link Configuration ─────────────────────────────────────────────────── + +const linking: LinkingOptions<{}> = { + prefixes: [`${DEEP_LINK_SCHEME}://`], + config: { + screens: { + MainTabs: { + screens: { + Home: 'home', + Scan: 'scan', + }, + }, + DevCardView: 'u/:username', + }, + }, +}; + +// ── App Content ─────────────────────────────────────────────────────────────── + function AppContent() { const { isAuthenticated, isLoading, login } = useAuth(); React.useEffect(() => { const handleDeepLink = (event: { url: string }) => { - console.log('--- DEEP LINK RECEIVED ---'); - console.log('URL:', event.url); - const url = new URL(event.url); - const token = url.searchParams.get('token'); - if (token) { - console.log('Token found, logging in...'); - login(token); + try { + const url = new URL(event.url); + const hashParams = new URLSearchParams(url.hash.replace(/^#/, '')); + const token = url.searchParams.get('token') || hashParams.get('token'); + if (token) { + login(token); + } + } catch (error) { + console.error('Deep link parse error:', error); } }; @@ -37,16 +60,18 @@ function AppContent() { }, [login]); if (isLoading) { - return null; // Splash screen could go here + return ; } return ( - + {isAuthenticated ? : } ); } +// ── Root ─────────────────────────────────────────────────────────────────────── + export default function App() { return ( diff --git a/apps/mobile/app.json b/apps/mobile/app.json index 20c74dff..2917e473 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -1,4 +1,5 @@ { "name": "DevCard", - "displayName": "DevCard" + "displayName": "DevCard", + "scheme": "devcard" } diff --git a/apps/mobile/babel.config.js b/apps/mobile/babel.config.js index 02c7d135..8ba8eb65 100644 --- a/apps/mobile/babel.config.js +++ b/apps/mobile/babel.config.js @@ -1,4 +1,4 @@ module.exports = { presets: ['module:@react-native/babel-preset'], - plugins: ['react-native-reanimated/plugin'], + plugins: ['react-native-worklets/plugin'], }; diff --git a/apps/mobile/index.js b/apps/mobile/index.js index fd5fb918..d5ce57df 100644 --- a/apps/mobile/index.js +++ b/apps/mobile/index.js @@ -1,7 +1,3 @@ -/** - * @format - */ - import 'react-native-gesture-handler'; import { AppRegistry } from 'react-native'; import App from './App'; diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js index ca00dd01..0d21ee3a 100644 --- a/apps/mobile/metro.config.js +++ b/apps/mobile/metro.config.js @@ -1,4 +1,4 @@ -const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const { getDefaultConfig } = require('@react-native/metro-config'); const path = require('path'); // Monorepo root @@ -6,21 +6,47 @@ const projectRoot = __dirname; const monorepoRoot = path.resolve(projectRoot, '../..'); /** - * Metro configuration for monorepo - * https://reactnative.dev/docs/metro - * - * @type {import('@react-native/metro-config').MetroConfig} + * Metro configuration for React Native monorepo */ -const config = { - watchFolders: [monorepoRoot], - resolver: { - nodeModulesPaths: [ - path.resolve(projectRoot, 'node_modules'), - path.resolve(monorepoRoot, 'node_modules'), - ], - // Ensure shared package is resolved - disableHierarchicalLookup: false, - }, -}; +module.exports = (async () => { + const config = await getDefaultConfig(projectRoot); -module.exports = mergeConfig(getDefaultConfig(__dirname), config); + config.watchFolders = [monorepoRoot]; + config.resolver = config.resolver || {}; + config.resolver.nodeModulesPaths = [ + path.resolve(projectRoot, 'node_modules'), + path.resolve(monorepoRoot, 'node_modules'), + ]; + config.resolver.disableHierarchicalLookup = false; + + const pinnedModules = { + react: path.resolve(projectRoot, 'node_modules/react'), + 'react-native': path.resolve(projectRoot, 'node_modules/react-native'), + 'react-native-reanimated': path.resolve( + projectRoot, + 'node_modules/react-native-reanimated' + ), + 'react-native-worklets': path.resolve( + projectRoot, + 'node_modules/react-native-worklets' + ), + 'react-native-gesture-handler': path.resolve( + projectRoot, + 'node_modules/react-native-gesture-handler' + ), + }; + + config.resolver.extraNodeModules = pinnedModules; + config.resolver.resolveRequest = (context, moduleName, platform) => { + for (const [name, modulePath] of Object.entries(pinnedModules)) { + if (moduleName === name || moduleName.startsWith(`${name}/`)) { + const target = path.join(modulePath, moduleName.slice(name.length)); + return context.resolveRequest(context, target, platform); + } + } + + return context.resolveRequest(context, moduleName, platform); + }; + + return config; +})(); diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 92fcba44..4cae19e2 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -2,6 +2,7 @@ "name": "@devcard/mobile", "version": "0.0.1", "private": true, + "main": "index.js", "scripts": { "android": "react-native run-android", "ios": "react-native run-ios", @@ -18,17 +19,20 @@ "@react-navigation/native": "^7.0.0", "@react-navigation/native-stack": "^7.0.0", "react": "19.2.3", - "react-dom": "^19.2.4", + "react-dom": "^19.2.3", "react-native": "0.84.1", - "react-native-gesture-handler": "^2.20.2", + "react-native-camera-kit": "^14.0.0", + "react-native-gesture-handler": "^2.28.0", "react-native-qrcode-svg": "^6.3.0", - "react-native-reanimated": "^3.15.0", + "react-native-reanimated": "^3.16.7", "react-native-safe-area-context": "^5.5.2", "react-native-screens": "^4.0.0", "react-native-svg": "^15.0.0", "react-native-vector-icons": "^10.0.0", + "react-native-view-shot": "^5.1.0", "react-native-web": "^0.21.2", - "react-native-webview": "^13.0.0" + "react-native-webview": "^13.0.0", + "react-native-worklets": "0.5.1" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/apps/mobile/src/components/Avatar.tsx b/apps/mobile/src/components/Avatar.tsx new file mode 100644 index 00000000..8a0ee0c8 --- /dev/null +++ b/apps/mobile/src/components/Avatar.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { View, Text, Image, ViewStyle, ImageStyle, StyleSheet } from 'react-native'; +import { COLORS } from '../theme/tokens'; + +type Props = { + uri?: string | null; + name?: string; + size?: number; + style?: ViewStyle | ImageStyle; +}; + +export const Avatar: React.FC = ({ uri, name = 'D', size = 56, style }) => { + const initials = name.charAt(0).toUpperCase(); + const imageStyle = [{ width: size, height: size, borderRadius: size / 2 } as ImageStyle, style as ImageStyle]; + const placeholderStyle = [{ width: size, height: size, borderRadius: size / 2, backgroundColor: COLORS.primary }, style as ViewStyle]; + + return uri ? ( + + ) : ( + + {initials} + + ); +}; + +export default Avatar; + +const styles = StyleSheet.create({ + placeholder: { + alignItems: 'center', + justifyContent: 'center', + }, + placeholderText: { + color: COLORS.white, + fontWeight: '800', + }, +}); diff --git a/apps/mobile/src/components/CardPickerSheet.tsx b/apps/mobile/src/components/CardPickerSheet.tsx index 44af9d93..7cbb12d3 100644 --- a/apps/mobile/src/components/CardPickerSheet.tsx +++ b/apps/mobile/src/components/CardPickerSheet.tsx @@ -8,6 +8,7 @@ import { BottomSheetScrollView, } from '@gorhom/bottom-sheet'; import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS } from '../theme/tokens'; +import { EmptyState } from './EmptyState'; type Props = { cards: Card[]; @@ -50,7 +51,10 @@ const CardPickerSheet = React.forwardRef( {cards.length === 0 ? ( - No cards yet + ) : ( cards.map(card => { @@ -144,12 +148,10 @@ const styles = StyleSheet.create({ textAlign: 'center', }, noCards: { - alignItems: 'center', - paddingVertical: SPACING.lg, - }, - noCardsText: { - fontSize: FONT_SIZE.sm, - color: COLORS.textMuted, + backgroundColor: COLORS.bgCard, + borderRadius: BORDER_RADIUS.md, + borderWidth: 1, + borderColor: COLORS.border, }, cardRow: { flexDirection: 'row', diff --git a/apps/mobile/src/components/ColorPicker.tsx b/apps/mobile/src/components/ColorPicker.tsx new file mode 100644 index 00000000..83eecd8f --- /dev/null +++ b/apps/mobile/src/components/ColorPicker.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { View, TouchableOpacity, StyleSheet } from 'react-native'; +import { COLORS, SPACING, BORDER_RADIUS } from '../theme/tokens'; + +// ── Predefined Accent Color Palette ─────────────────────────────────────────── +// 8 curated colors that work well as card accent on the dark DevCard theme. + +export const ACCENT_COLORS = [ + '#6366F1', // Indigo (default) + '#8B5CF6', // Violet + '#EC4899', // Pink + '#EF4444', // Red + '#F59E0B', // Amber + '#22C55E', // Green + '#06B6D4', // Cyan + '#3B82F6', // Blue +] as const; + +export type AccentColor = (typeof ACCENT_COLORS)[number]; + +interface ColorPickerProps { + selected: string; + onSelect: (color: string) => void; +} + +export default function ColorPicker({ selected, onSelect }: ColorPickerProps) { + return ( + + {ACCENT_COLORS.map((color) => { + const isActive = selected === color; + return ( + onSelect(color)} + activeOpacity={0.7} + accessibilityLabel={`Select accent color ${color}`} + accessibilityRole="radio" + accessibilityState={{ selected: isActive }} + /> + ); + })} + + ); +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: SPACING.sm, + justifyContent: 'center', + }, + swatch: { + width: 40, + height: 40, + borderRadius: BORDER_RADIUS.full, + borderWidth: 2, + borderColor: COLORS.transparent, + }, + swatchActive: { + borderColor: COLORS.white, + transform: [{ scale: 1.15 }], + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.4, + shadowRadius: 4, + elevation: 6, + }, +}); diff --git a/apps/mobile/src/components/EmptyState.tsx b/apps/mobile/src/components/EmptyState.tsx new file mode 100644 index 00000000..2ad886db --- /dev/null +++ b/apps/mobile/src/components/EmptyState.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { COLORS, SPACING, FONT_SIZE } from '../theme/tokens'; + +interface EmptyStateProps { + emoji?: string; + title: string; + description?: string; +} + +export const EmptyState: React.FC = ({ emoji, title, description }) => ( + + {emoji ? {emoji} : null} + {title} + {description ? {description} : null} + +); + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + paddingVertical: SPACING.xxl, + paddingHorizontal: SPACING.lg, + }, + emoji: { + fontSize: 48, + marginBottom: SPACING.md, + }, + title: { + fontSize: FONT_SIZE.lg, + fontWeight: '700', + color: COLORS.textPrimary, + textAlign: 'center', + }, + description: { + marginTop: SPACING.xs, + fontSize: FONT_SIZE.sm, + color: COLORS.textMuted, + textAlign: 'center', + lineHeight: 20, + }, +}); diff --git a/apps/mobile/src/components/LoadingPlaceholder.tsx b/apps/mobile/src/components/LoadingPlaceholder.tsx new file mode 100644 index 00000000..22f5b211 --- /dev/null +++ b/apps/mobile/src/components/LoadingPlaceholder.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { Skeleton } from './Skeleton'; +import { SPACING, BORDER_RADIUS, COLORS } from '../theme/tokens'; + +interface LoadingPlaceholderProps { + rows?: number; +} + +export const LoadingPlaceholder: React.FC = ({ rows = 3 }) => ( + + {Array.from({ length: rows }).map((_, index) => ( + + + + + + + + ))} + +); + +const styles = StyleSheet.create({ + container: { + padding: SPACING.lg, + }, + item: { + flexDirection: 'row', + alignItems: 'center', + gap: SPACING.md, + marginBottom: SPACING.md, + backgroundColor: COLORS.bgCard, + borderRadius: BORDER_RADIUS.lg, + padding: SPACING.md, + }, + textColumn: { + flex: 1, + justifyContent: 'center', + }, + secondLine: { + marginTop: SPACING.xs, + }, +}); diff --git a/apps/mobile/src/components/Skeleton.tsx b/apps/mobile/src/components/Skeleton.tsx index 23f52d27..4c65e855 100644 --- a/apps/mobile/src/components/Skeleton.tsx +++ b/apps/mobile/src/components/Skeleton.tsx @@ -1,10 +1,10 @@ import React, { useEffect, useRef } from 'react'; -import { View, Animated, StyleSheet, ViewStyle } from 'react-native'; +import { Animated, StyleSheet, ViewStyle, DimensionValue } from 'react-native'; import { COLORS } from '../theme/tokens'; interface SkeletonProps { - width?: number | string; - height?: number | string; + width?: DimensionValue; + height?: DimensionValue; borderRadius?: number; style?: ViewStyle; } diff --git a/apps/mobile/src/config.ts b/apps/mobile/src/config.ts index 7d3e7dda..3ef038e2 100644 --- a/apps/mobile/src/config.ts +++ b/apps/mobile/src/config.ts @@ -1,12 +1,22 @@ -// DevCard API Configuration +import { Platform } from 'react-native'; -// For Android emulator, use localhost with 'adb reverse tcp:3000 tcp:3000' -export const API_BASE_URL = __DEV__ - ? 'http://localhost:3000' +// ── DevCard API Configuration ───────────────────────────────────────────────── +// Environment-aware URLs with no Expo dependency. On Android emulators the +// loopback address is 10.0.2.2; on iOS simulators localhost works directly. + +const ANDROID_LOCALHOST = '10.0.2.2'; +const IOS_LOCALHOST = 'localhost'; +const DEV_HOST = Platform.OS === 'android' ? ANDROID_LOCALHOST : IOS_LOCALHOST; + +export const API_BASE_URL: string = __DEV__ + ? `http://${DEV_HOST}:3000` : 'https://api.devcard.dev'; -export const APP_URL = __DEV__ +export const APP_URL: string = __DEV__ ? 'http://localhost:5173' : 'https://devcard.dev'; -export const OAUTH_REDIRECT_URI = 'devcard://oauth/callback'; +// Deep link scheme — must match android/app/build.gradle and ios/Info.plist +export const DEEP_LINK_SCHEME = 'devcard'; + +export const OAUTH_REDIRECT_URI = `${DEEP_LINK_SCHEME}://oauth/callback`; diff --git a/apps/mobile/src/context/AuthContext.tsx b/apps/mobile/src/context/AuthContext.tsx index 77559e4c..343e103c 100644 --- a/apps/mobile/src/context/AuthContext.tsx +++ b/apps/mobile/src/context/AuthContext.tsx @@ -1,5 +1,13 @@ -import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; -import { API_BASE_URL } from '../config'; +import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { get } from '../services/api'; + +// ── Storage Keys ────────────────────────────────────────────────────────────── + +const TOKEN_KEY = 'devcard.auth.token'; +const FIRST_LAUNCH_KEY = 'devcard.firstLaunch'; + +// ── Types ───────────────────────────────────────────────────────────────────── interface User { id: string; @@ -20,59 +28,99 @@ interface AuthContextType { token: string | null; isAuthenticated: boolean; isLoading: boolean; + isFirstLaunch: boolean; login: (token: string) => Promise; - logout: () => void; + logout: () => Promise; refreshUser: () => Promise; } +// ── Context ─────────────────────────────────────────────────────────────────── + const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); const [token, setToken] = useState(null); const [isLoading, setIsLoading] = useState(true); + const [isFirstLaunch, setIsFirstLaunch] = useState(false); + + // ── Hydrate token from AsyncStorage on mount ── useEffect(() => { - // TODO: Load token from secure storage on app start - setIsLoading(false); + const hydrate = async () => { + try { + const [storedToken, launchFlag] = await Promise.all([ + AsyncStorage.getItem(TOKEN_KEY), + AsyncStorage.getItem(FIRST_LAUNCH_KEY), + ]); + + if (launchFlag === null) { + setIsFirstLaunch(true); + await AsyncStorage.setItem(FIRST_LAUNCH_KEY, 'false'); + } + + if (storedToken) { + setToken(storedToken); + // Validate token by fetching profile + const userData = await get('/api/profiles/me', storedToken).catch(() => null); + if (userData) { + setUser(userData); + } else { + // Token expired or invalid — clear it + await AsyncStorage.removeItem(TOKEN_KEY); + setToken(null); + } + } + } catch (error) { + console.error('Auth hydration failed:', error); + } finally { + setIsLoading(false); + } + }; + + hydrate(); }, []); - const login = async (newToken: string) => { + // ── Login ── + + const login = useCallback(async (newToken: string) => { setToken(newToken); - // TODO: Save token to secure storage try { - const res = await fetch(`${API_BASE_URL}/api/profiles/me`, { - headers: { Authorization: `Bearer ${newToken}` }, - }); - if (res.ok) { - const userData = await res.json(); + await AsyncStorage.setItem(TOKEN_KEY, newToken); + const userData = await get('/api/profiles/me', newToken).catch(() => null); + if (userData) { setUser(userData); } - } catch (err) { - console.error('Failed to fetch user:', err); + } catch (error) { + console.error('Failed to persist token or fetch user:', error); } - }; + }, []); + + // ── Logout ── - const logout = () => { + const logout = useCallback(async () => { setToken(null); setUser(null); - // TODO: Clear token from secure storage - }; + try { + await AsyncStorage.removeItem(TOKEN_KEY); + } catch (error) { + console.error('Failed to clear stored token:', error); + } + }, []); + + // ── Refresh User ── - const refreshUser = async () => { + const refreshUser = useCallback(async () => { if (!token) return; try { - const res = await fetch(`${API_BASE_URL}/api/profiles/me`, { - headers: { Authorization: `Bearer ${token}` }, - }); - if (res.ok) { - const userData = await res.json(); + const userData = await get('/api/profiles/me', token).catch(() => null); + if (userData) { setUser(userData); } - } catch (err) { - console.error('Failed to refresh user:', err); + } catch (error) { + console.error('Failed to refresh user:', error); } - }; + }, [token]); return ( { + data: T | null; + loading: boolean; + error: string | null; + refetch: () => Promise; +} + +interface UseApiQueryOptions { + /** Skip the initial fetch (useful for conditional queries) */ + skip?: boolean; +} + +export function useApiQuery( + path: string, + options: UseApiQueryOptions = {}, +): UseApiQueryResult { + const { token, logout } = useAuth(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(!options.skip); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + if (!token) { + setLoading(false); + return; + } + setLoading(true); + setError(null); + try { + const result = await apiRequest(path, { + method: 'GET', + token, + onUnauthorized: logout, + }); + setData(result); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : 'Request failed'; + setError(message); + console.error(`useApiQuery(${path}):`, message); + } finally { + setLoading(false); + } + }, [path, token, logout]); + + useEffect(() => { + if (!options.skip) { + fetchData(); + } + }, [fetchData, options.skip]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/apps/mobile/src/hooks/useContacts.ts b/apps/mobile/src/hooks/useContacts.ts new file mode 100644 index 00000000..99b59b2a --- /dev/null +++ b/apps/mobile/src/hooks/useContacts.ts @@ -0,0 +1,90 @@ +import { useState, useEffect, useCallback } from 'react'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import type { SavedContact } from '../types'; + +// ── Storage Key ─────────────────────────────────────────────────────────────── + +const CONTACTS_KEY = 'devcard.contacts'; + +// ── Hook ────────────────────────────────────────────────────────────────────── + +interface UseContactsResult { + contacts: SavedContact[]; + loading: boolean; + saveContact: (contact: Omit) => Promise; + removeContact: (username: string) => Promise; + isContactSaved: (username: string) => boolean; + refetch: () => Promise; +} + +export function useContacts(): UseContactsResult { + const [contacts, setContacts] = useState([]); + const [loading, setLoading] = useState(true); + + const loadContacts = useCallback(async () => { + try { + const raw = await AsyncStorage.getItem(CONTACTS_KEY); + if (raw) { + const parsed: SavedContact[] = JSON.parse(raw); + // Sort by most recently saved first + parsed.sort((a, b) => new Date(b.savedAt).getTime() - new Date(a.savedAt).getTime()); + setContacts(parsed); + } else { + setContacts([]); + } + } catch (error) { + console.error('Failed to load contacts:', error); + setContacts([]); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + loadContacts(); + }, [loadContacts]); + + const persistContacts = async (updated: SavedContact[]) => { + try { + await AsyncStorage.setItem(CONTACTS_KEY, JSON.stringify(updated)); + setContacts(updated); + } catch (error) { + console.error('Failed to persist contacts:', error); + } + }; + + const saveContact = useCallback( + async (contact: Omit) => { + const existing = contacts.filter((c) => c.username !== contact.username); + const newContact: SavedContact = { + ...contact, + savedAt: new Date().toISOString(), + }; + const updated = [newContact, ...existing]; + await persistContacts(updated); + }, + [contacts], + ); + + const removeContact = useCallback( + async (username: string) => { + const updated = contacts.filter((c) => c.username !== username); + await persistContacts(updated); + }, + [contacts], + ); + + const isContactSaved = useCallback( + (username: string) => contacts.some((c) => c.username === username), + [contacts], + ); + + return { + contacts, + loading, + saveContact, + removeContact, + isContactSaved, + refetch: loadContacts, + }; +} diff --git a/apps/mobile/src/navigation/MainTabs.tsx b/apps/mobile/src/navigation/MainTabs.tsx index 11e4e9a4..74cb88af 100644 --- a/apps/mobile/src/navigation/MainTabs.tsx +++ b/apps/mobile/src/navigation/MainTabs.tsx @@ -11,26 +11,49 @@ import SettingsScreen from '../screens/SettingsScreen'; import ScanScreen from '../screens/ScanScreen'; import DevCardViewScreen from '../screens/DevCardViewScreen'; import WebViewScreen from '../screens/WebViewScreen'; +import ContactsScreen from '../screens/ContactsScreen'; +import EventsScreen from '../screens/EventsScreen'; +import EventDetailScreen from '../screens/EventDetailScreen'; +import TeamsScreen from '../screens/TeamsScreen'; +import TeamDetailScreen from '../screens/TeamDetailScreen'; +import NfcScreen from '../screens/NfcScreen'; -import ConnectPlatformsScreen from '../screens/ConnectPlatformsScreen'; -import ViewsScreen from '../screens/ViewsScreen'; +import { ConnectPlatformsScreen } from '../screens/ConnectPlatformsScreen'; +import { ViewsScreen } from '../screens/ViewsScreen'; // ─── Types ─── export type MainTabsParamList = { Home: undefined; - Links: undefined; + Contacts: undefined; Scan: undefined; Cards: undefined; Settings: undefined; }; +// Standalone type for WebViewConnect route params — exported for reuse in +// WebViewScreen, DevCardViewScreen, or any future screen that navigates here. +export type WebViewConnectParams = { + platform: string; + url: string; + platformName: string; + username?: string; + linkId?: string; + cardOwnerUsername: string; +}; + export type RootStackParamList = { MainTabs: undefined; - DevCardView: { username: string }; - WebViewConnect: { platform: string; profileUrl: string; displayName: string }; + DevCardView: { username: string; followSuccessLinkId?: string }; + WebViewConnect: WebViewConnectParams; ConnectPlatforms: undefined; Views: undefined; + Links: undefined; + Events: undefined; + EventDetail: { slug: string; name: string }; + Teams: undefined; + TeamDetail: { slug: string; name: string }; + Nfc: undefined; }; // ─── Tab Bar Icon ─── @@ -38,7 +61,7 @@ export type RootStackParamList = { function TabIcon({ name, focused }: { name: string; focused: boolean }) { const icons: Record = { Home: '🏠', - Links: '🔗', + Contacts: '📇', Scan: '📷', Cards: '💳', Settings: '⚙️', @@ -52,6 +75,14 @@ function TabIcon({ name, focused }: { name: string; focused: boolean }) { ); } +function ScanButton() { + return ( + + 📷 + + ); +} + // ─── Tab Navigator ─── const Tab = createBottomTabNavigator(); @@ -70,17 +101,13 @@ function TabNavigator() { ), })}> - + ( - - 📷 - - ), + tabBarIcon: () => , }} /> @@ -117,6 +144,12 @@ export default function MainTabs() { component={ViewsScreen} options={{ title: 'Card Views Analytics', headerShown: true, headerStyle: { backgroundColor: COLORS.bgPrimary }, headerTintColor: COLORS.textPrimary }} /> + + + + + + ); } diff --git a/apps/mobile/src/screens/CardsScreen.tsx b/apps/mobile/src/screens/CardsScreen.tsx index 023ceb42..6953ffd2 100644 --- a/apps/mobile/src/screens/CardsScreen.tsx +++ b/apps/mobile/src/screens/CardsScreen.tsx @@ -16,7 +16,9 @@ import { useFocusEffect } from '@react-navigation/native'; import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS, SHADOWS } from '../theme/tokens'; import { useAuth } from '../context/AuthContext'; import { PLATFORMS } from '@devcard/shared'; -import { API_BASE_URL } from '../config'; +import { get, post, del, put } from '../services/api'; +import { EmptyState } from '../components/EmptyState'; +import { Skeleton } from '../components/Skeleton'; interface PlatformLink { id: string; @@ -39,26 +41,22 @@ export default function CardsScreen() { const [newTitle, setNewTitle] = useState(''); const [selectedLinkIds, setSelectedLinkIds] = useState([]); const [refreshing, setRefreshing] = useState(false); + const [loading, setLoading] = useState(true); - const fetchData = useCallback(async () => { + const fetchData = useCallback(async (showLoading = true) => { + if (showLoading) setLoading(true); try { - const [cardsRes, profileRes] = await Promise.all([ - fetch(`${API_BASE_URL}/api/cards`, { - headers: { Authorization: `Bearer ${token}` }, - }), - fetch(`${API_BASE_URL}/api/profiles/me`, { - headers: { Authorization: `Bearer ${token}` }, - }), + const [cardsData, profileData] = await Promise.all([ + get('/api/cards', token).catch(() => []), + get('/api/profiles/me', token).catch(() => null), ]); - if (cardsRes.ok) setCards(await cardsRes.json()); - if (profileRes.ok) { - const data = await profileRes.json(); - setAllLinks(data.platformLinks || []); - } - } catch (err) { - console.error('Failed to fetch:', err); + setCards(cardsData || []); + setAllLinks(profileData?.platformLinks || []); + } catch (error) { + console.error('Failed to fetch:', error); } finally { setRefreshing(false); + if (showLoading) setLoading(false); } }, [token]); @@ -70,7 +68,7 @@ export default function CardsScreen() { const onRefresh = () => { setRefreshing(true); - fetchData(); + fetchData(false); }; const createCard = async () => { @@ -79,21 +77,12 @@ export default function CardsScreen() { return; } try { - const res = await fetch(`${API_BASE_URL}/api/cards`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ title: newTitle.trim(), linkIds: selectedLinkIds }), - }); - if (res.ok) { - setShowCreate(false); - setNewTitle(''); - setSelectedLinkIds([]); - fetchData(); - } - } catch (err) { + await post('/api/cards', { title: newTitle.trim(), linkIds: selectedLinkIds }, token); + setShowCreate(false); + setNewTitle(''); + setSelectedLinkIds([]); + fetchData(); + } catch { Alert.alert('Error', 'Failed to create card'); } }; @@ -105,10 +94,11 @@ export default function CardsScreen() { text: 'Delete', style: 'destructive', onPress: async () => { - await fetch(`${API_BASE_URL}/api/cards/${id}`, { - method: 'DELETE', - headers: { Authorization: `Bearer ${token}` }, - }); + try { + await del(`/api/cards/${id}`, undefined, token); + } catch { + // ignore + } fetchData(); }, }, @@ -116,10 +106,11 @@ export default function CardsScreen() { }; const setDefault = async (id: string) => { - await fetch(`${API_BASE_URL}/api/cards/${id}/default`, { - method: 'PUT', - headers: { Authorization: `Bearer ${token}` }, - }); + try { + await put(`/api/cards/${id}/default`, undefined, token); + } catch { + // ignore + } fetchData(); }; @@ -131,6 +122,29 @@ export default function CardsScreen() { ); }; + if (loading) { + return ( + + + + + + + + {[1, 2].map((item) => ( + + + + + + + + ))} + + + ); + } + return ( @@ -211,11 +225,11 @@ export default function CardsScreen() { )} ListEmptyComponent={ - - 💳 - No cards yet - Create context cards for different situations - + } /> @@ -283,6 +297,19 @@ const styles = StyleSheet.create({ emptyEmoji: { fontSize: 48, marginBottom: SPACING.md }, emptyText: { fontSize: FONT_SIZE.lg, fontWeight: '600', color: COLORS.textPrimary }, emptySubtext: { fontSize: FONT_SIZE.sm, color: COLORS.textMuted, marginTop: SPACING.xs }, + loadingList: { paddingHorizontal: SPACING.lg }, + loadingCard: { + borderRadius: BORDER_RADIUS.lg, + backgroundColor: COLORS.bgCard, + padding: SPACING.md, + marginBottom: SPACING.lg, + }, + loadingActionRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginTop: SPACING.md, + }, modalOverlay: { flex: 1, backgroundColor: COLORS.overlay, justifyContent: 'flex-end' }, modalContent: { backgroundColor: COLORS.bgSecondary, borderTopLeftRadius: BORDER_RADIUS.xl, diff --git a/apps/mobile/src/screens/ConnectPlatformsScreen.tsx b/apps/mobile/src/screens/ConnectPlatformsScreen.tsx index 8b359ca7..2e59ed11 100644 --- a/apps/mobile/src/screens/ConnectPlatformsScreen.tsx +++ b/apps/mobile/src/screens/ConnectPlatformsScreen.tsx @@ -1,10 +1,12 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { View, Text, StyleSheet, ScrollView, ActivityIndicator, TouchableOpacity, Alert, Linking } from 'react-native'; +import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert, Linking } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS } from '../theme/tokens'; import { useAuth } from '../context/AuthContext'; import { API_BASE_URL } from '../config'; +import { get, del } from '../services/api'; +import { LoadingPlaceholder } from '../components/LoadingPlaceholder'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import type { RootStackParamList } from '../navigation/MainTabs'; @@ -27,15 +29,10 @@ export const ConnectPlatformsScreen: React.FC = ({ navigation: _navigatio return; } try { - const response = await fetch(`${API_BASE_URL}/api/connect/status`, { - headers: { Authorization: `Bearer ${token}` }, - }); - if (response.ok) { - const data = await response.json(); - setConnectedPlatforms(data.connectedPlatforms || []); - } - } catch (err) { - console.error('Failed to fetch connected platforms', err); + const data = await get('/api/connect/status', token).catch(() => null); + setConnectedPlatforms(data?.connectedPlatforms || []); + } catch (error) { + console.error('Failed to fetch connected platforms', error); } finally { setLoading(false); } @@ -78,15 +75,8 @@ export const ConnectPlatformsScreen: React.FC = ({ navigation: _navigatio onPress: async () => { try { if (!token) return; - const response = await fetch(`${API_BASE_URL}/api/connect/${platform}`, { - method: 'DELETE', - headers: { Authorization: `Bearer ${token}` }, - }); - if (response.ok) { - fetchConnections(); - } else { - Alert.alert('Error', 'Failed to disconnect'); - } + await del(`/api/connect/${platform}`, undefined, token); + fetchConnections(); } catch { Alert.alert('Error', 'Failed to disconnect'); } @@ -136,9 +126,9 @@ export const ConnectPlatformsScreen: React.FC = ({ navigation: _navigatio if (loading) { return ( - - - + + + ); } diff --git a/apps/mobile/src/screens/ContactsScreen.tsx b/apps/mobile/src/screens/ContactsScreen.tsx new file mode 100644 index 00000000..a657592a --- /dev/null +++ b/apps/mobile/src/screens/ContactsScreen.tsx @@ -0,0 +1,169 @@ +import React, { useCallback } from 'react'; +import { + View, + Text, + StyleSheet, + FlatList, + TouchableOpacity, + Alert, + StatusBar, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useFocusEffect } from '@react-navigation/native'; +import Avatar from '../components/Avatar'; +import { EmptyState } from '../components/EmptyState'; +import { LoadingPlaceholder } from '../components/LoadingPlaceholder'; +import { useContacts } from '../hooks/useContacts'; +import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS } from '../theme/tokens'; +import type { SavedContact } from '../types'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import type { RootStackParamList } from '../navigation/MainTabs'; + +type Props = { + navigation: NativeStackNavigationProp; +}; + +export default function ContactsScreen({ navigation }: Props) { + const { contacts, loading, removeContact, refetch } = useContacts(); + + useFocusEffect( + useCallback(() => { + refetch(); + }, [refetch]), + ); + + const handlePress = (contact: SavedContact) => { + navigation.navigate('DevCardView', { username: contact.username }); + }; + + const handleRemove = (contact: SavedContact) => { + Alert.alert( + 'Remove Contact', + `Remove ${contact.displayName} from saved contacts?`, + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Remove', + style: 'destructive', + onPress: () => removeContact(contact.username), + }, + ], + ); + }; + + const formatDate = (dateString: string) => { + const d = new Date(dateString); + return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); + }; + + if (loading) { + return ( + + + + + ); + } + + return ( + + + + + Saved Contacts + {contacts.length} + + + item.username} + contentContainerStyle={styles.list} + renderItem={({ item }) => ( + handlePress(item)} + onLongPress={() => handleRemove(item)} + activeOpacity={0.7}> + + + + {item.displayName} + + {item.role || item.company ? ( + + {[item.role, item.company].filter(Boolean).join(' · ')} + + ) : null} + {item.metAt ? ( + + Met at {item.metAt} + + ) : null} + + + + {formatDate(item.savedAt)} + + + )} + ListEmptyComponent={ + + } + /> + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: COLORS.bgPrimary }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: SPACING.lg, + paddingBottom: SPACING.md, + }, + title: { fontSize: FONT_SIZE.xl, fontWeight: '800', color: COLORS.textPrimary }, + count: { + fontSize: FONT_SIZE.sm, + fontWeight: '700', + color: COLORS.textMuted, + backgroundColor: COLORS.bgElevated, + borderRadius: BORDER_RADIUS.full, + paddingHorizontal: SPACING.sm, + paddingVertical: 2, + overflow: 'hidden', + }, + list: { padding: SPACING.lg, gap: SPACING.sm, paddingTop: 0 }, + contactItem: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: COLORS.bgCard, + borderRadius: BORDER_RADIUS.md, + padding: SPACING.md, + borderWidth: 1, + borderColor: COLORS.border, + }, + avatar: { + width: 48, + height: 48, + borderRadius: 24, + marginRight: SPACING.md, + }, + info: { flex: 1 }, + name: { fontSize: FONT_SIZE.md, fontWeight: '600', color: COLORS.textPrimary }, + detail: { fontSize: FONT_SIZE.sm, color: COLORS.textSecondary, marginTop: 2 }, + metAt: { fontSize: FONT_SIZE.xs, color: COLORS.primary, marginTop: 2 }, + meta: { alignItems: 'flex-end', gap: 4 }, + accentDot: { width: 10, height: 10, borderRadius: 5 }, + date: { fontSize: FONT_SIZE.xs, color: COLORS.textMuted }, +}); diff --git a/apps/mobile/src/screens/DevCardViewScreen.tsx b/apps/mobile/src/screens/DevCardViewScreen.tsx index 46cf9519..7d6de992 100644 --- a/apps/mobile/src/screens/DevCardViewScreen.tsx +++ b/apps/mobile/src/screens/DevCardViewScreen.tsx @@ -1,11 +1,10 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, - Image, Linking, Clipboard, StatusBar, @@ -15,9 +14,12 @@ import { import { SafeAreaView } from 'react-native-safe-area-context'; import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS, SHADOWS } from '../theme/tokens'; import { Skeleton } from '../components/Skeleton'; +import { EmptyState } from '../components/EmptyState'; +import Avatar from '../components/Avatar'; import { PLATFORMS, getProfileUrl, getWebViewUrl } from '@devcard/shared'; -import { API_BASE_URL } from '../config'; +import { get, post, del } from '../services/api'; import { useAuth } from '../context/AuthContext'; +import { useContacts } from '../hooks/useContacts'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RouteProp } from '@react-navigation/native'; import type { RootStackParamList } from '../navigation/MainTabs'; @@ -50,29 +52,106 @@ interface ProfileData { type FollowState = Record; +// ─── Platform Emoji Icon Map ─── +const PLATFORM_EMOJI: Record = { + github: '🐙', + linkedin: 'in', + twitter: '𝕏', + gitlab: '🦊', + devfolio: '🏗️', + npm: '📦', + devto: '👩‍💻', + hashnode: '📝', + medium: 'M', + leetcode: '🏆', + hackerrank: '⚔️', + stackoverflow: '💬', + discord: '🎮', + telegram: '✈️', + email: '✉️', + portfolio: '🌐', + custom: '🔗', +}; + +// ─── Brand-colored action buttons ─── +const PLATFORM_BTN_COLOR: Record = { + github: '#238636', + linkedin: '#0A66C2', + twitter: '#1D9BF0', + gitlab: '#FC6D26', + devfolio: '#3770FF', + npm: '#CB3837', + devto: '#3B49DF', + leetcode: '#FFA116', + hackerrank: '#00B86B', + stackoverflow: '#F58025', + discord: '#5865F2', + telegram: '#26A5E4', + email: '#EA4335', + portfolio: '#6366F1', +}; + export default function DevCardViewScreen({ navigation, route }: Props) { const { username } = route.params; const { token } = useAuth(); + const { isContactSaved, saveContact, removeContact } = useContacts(); const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(true); const [followStates, setFollowStates] = useState({}); - useEffect(() => { - fetchProfile(); - }, [username]); + const isSaved = isContactSaved(username); + + const handleSaveContact = async () => { + if (!profile) return; + if (isSaved) { + await removeContact(username); + } else { + await saveContact({ + username: profile.username, + displayName: profile.displayName, + avatarUrl: profile.avatarUrl, + accentColor: profile.accentColor || COLORS.primary, + bio: profile.bio, + role: profile.role, + company: profile.company, + metAt: 'DevCard App', + note: null, + }); + Alert.alert('Saved!', `${profile.displayName} has been added to your contacts.`); + } + }; - const fetchProfile = async () => { + const fetchProfile = useCallback(async () => { try { - const res = await fetch(`${API_BASE_URL}/api/u/${username}`); - if (res.ok) { - setProfile(await res.json()); + const data = await get(`/api/u/${username}`, token); + if (data) { + setProfile(data); + const initialFollowStates: FollowState = {}; + if (data.links) { + data.links.forEach((link: any) => { + if (link.followed) initialFollowStates[link.id] = 'success'; + }); + } + setFollowStates(initialFollowStates); } - } catch (err) { - console.error('Failed to fetch profile:', err); + } catch (error) { + console.error('Failed to fetch profile:', error); } finally { setLoading(false); } - }; + }, [token, username]); + + useEffect(() => { + fetchProfile(); + }, [fetchProfile]); + + const successLinkId = route.params?.followSuccessLinkId; + useEffect(() => { + if (successLinkId) { + setFollowStates(prev => ({ ...prev, [successLinkId]: 'success' })); + navigation.setParams({ followSuccessLinkId: undefined } as any); + } + }, [navigation, successLinkId]); // ─── Hybrid Follow Engine ─── @@ -84,17 +163,26 @@ export default function DevCardViewScreen({ navigation, route }: Props) { switch (strategy) { case 'api': - // Layer 1: Silent API follow await handleApiFollow(link); break; case 'webview': - // Layer 2: WebView connect - handleWebViewConnect(link); + setFollowStates(prev => ({ ...prev, [link.id]: 'loading' })); + try { + const data = await post(`/api/follow/${link.platform}/${link.username}`, undefined, token); + setFollowStates(prev => ({ ...prev, [link.id]: 'idle' })); + if (data?.strategy === 'webview') { + handleWebViewConnect(link, data.url); + } else { + setFollowStates(prev => ({ ...prev, [link.id]: 'success' })); + } + } catch { + setFollowStates(prev => ({ ...prev, [link.id]: 'idle' })); + handleWebViewConnect(link); + } break; case 'copy': - // Copy to clipboard (Discord) Clipboard.setString(link.username); Alert.alert('Copied!', `${link.username} copied to clipboard`); setFollowStates(prev => ({ ...prev, [link.id]: 'success' })); @@ -102,7 +190,6 @@ export default function DevCardViewScreen({ navigation, route }: Props) { case 'link': default: - // Layer 3: Open in browser/app const url = link.url || getProfileUrl(link.platform, link.username); if (url) { Linking.openURL(url).catch(() => @@ -117,40 +204,49 @@ export default function DevCardViewScreen({ navigation, route }: Props) { const handleApiFollow = async (link: PlatformLink) => { setFollowStates(prev => ({ ...prev, [link.id]: 'loading' })); try { - const res = await fetch( - `${API_BASE_URL}/api/follow/${link.platform}/${link.username}`, - { - method: 'POST', - headers: { Authorization: `Bearer ${token}` }, - } - ); - if (res.ok) { - setFollowStates(prev => ({ ...prev, [link.id]: 'success' })); - } else { - const data = await res.json(); - if (data.requiresAuth) { - // Fall back to WebView if token missing + await post(`/api/follow/${link.platform}/${link.username}`, undefined, token); + setFollowStates(prev => ({ ...prev, [link.id]: 'success' })); + } catch (err: any) { + const msg = (err && err.message) || ''; + if (msg.includes('requiresAuth')) { + setFollowStates(prev => ({ ...prev, [link.id]: 'idle' })); + const webViewUrl = getWebViewUrl(link.platform, link.username); + if (webViewUrl) { handleWebViewConnect(link); } else { - setFollowStates(prev => ({ ...prev, [link.id]: 'error' })); + const profileUrl = link.url || getProfileUrl(link.platform, link.username); + if (profileUrl) Linking.openURL(profileUrl).catch(() => Alert.alert('Error', `Could not open ${link.platform} profile`)); } + } else { + setFollowStates(prev => ({ ...prev, [link.id]: 'error' })); } + } + }; + + // Reset a "Done" tile — clears follow log from backend and resets local state + const handleResetFollowState = async (link: PlatformLink) => { + try { + await del(`/api/follow/${link.platform}/${link.username}/log`, undefined, token); } catch { - setFollowStates(prev => ({ ...prev, [link.id]: 'error' })); + // ignore } + setFollowStates(prev => ({ ...prev, [link.id]: 'idle' })); }; // Layer 2: WebView-based connect - const handleWebViewConnect = (link: PlatformLink) => { + const handleWebViewConnect = (link: PlatformLink, resolvedUrl?: string) => { const webViewUrl = getWebViewUrl(link.platform, link.username); const profileUrl = link.url || getProfileUrl(link.platform, link.username); - const url = webViewUrl || profileUrl; + const url = resolvedUrl || webViewUrl || profileUrl; if (url) { navigation.navigate('WebViewConnect', { platform: link.platform, - profileUrl: url, - displayName: PLATFORMS[link.platform]?.name || link.platform, + url, + platformName: PLATFORMS[link.platform]?.name || link.platform, + username: link.username, + linkId: link.id, + cardOwnerUsername: username, }); } }; @@ -173,20 +269,27 @@ export default function DevCardViewScreen({ navigation, route }: Props) { } }; + const getButtonColor = (link: PlatformLink, state: string): string => { + if (state === 'success') return COLORS.success; + if (state === 'loading') return COLORS.primaryDark; + if (state === 'error') return '#DC2626'; + return PLATFORM_BTN_COLOR[link.platform] || COLORS.primary; + }; + if (loading) { return ( {/* Header Skeleton */} - + - + @@ -198,15 +301,15 @@ export default function DevCardViewScreen({ navigation, route }: Props) { {/* Tiles Skeleton */} - + {[1, 2, 3].map(i => ( - - - + + + - + ))} @@ -238,93 +341,138 @@ export default function DevCardViewScreen({ navigation, route }: Props) { + {/* Save Contact Button */} + {profile && ( + + + {isSaved ? 'Saved' : 'Save'} + + + )} + - {/* Profile Card — PREMIUM REDESIGN */} - + {/* Profile Card */} + + {/* Gradient layers */} + - + + {/* Top row: brand + contactless */} - + DevCard PRO - 📶 + + PLATINUM + + {/* Middle: avatar + name/role */} - - {profile.avatarUrl ? ( - - ) : ( - - - {profile.displayName.charAt(0).toUpperCase()} - - - )} + + - {profile.displayName} - - {profile.role}{profile.company ? ` @ ${profile.company}` : ''} - + {profile.displayName} + {(profile.role || profile.company) && ( + + {profile.role}{profile.company ? ` @ ${profile.company}` : ''} + + )} {profile.pronouns && ( {profile.pronouns} )} - - - {profile.bio && {profile.bio}} - - - PLATINUM + {/* Bottom: bio + divider */} + {profile.bio ? ( + + + {profile.bio} - + ) : null} {/* Platform Tiles Section */} - Digital Touchpoints - {profile.links.map(link => { + + Digital Touchpoints + + {profile.links.length} + + + + {profile.links.length === 0 ? ( + + + + ) : profile.links.map(link => { const platform = PLATFORMS[link.platform]; const state = followStates[link.id] || 'idle'; + const btnColor = getButtonColor(link, state); + const isDone = state === 'success'; + const tileIconDynamic = isDone + ? { backgroundColor: 'rgba(34,197,94,0.12)', borderColor: COLORS.success } + : { backgroundColor: (platform?.color || COLORS.primary) + '22', borderColor: (platform?.color || COLORS.primary) + '66' }; return ( handlePlatformAction(link)} - activeOpacity={0.8} + onLongPress={() => { + if (isDone) { + Alert.alert( + 'Reset connection?', + `This will clear the "Done" status for ${platform?.name || link.platform}.`, + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Reset', + style: 'destructive', + onPress: () => handleResetFollowState(link), + }, + ] + ); + } + }} + activeOpacity={isDone ? 0.9 : 0.8} disabled={state === 'loading'}> - - - {platform?.name.charAt(0) || '?'} - + + {/* Icon */} + + {isDone ? ( + + ) : ( + + {PLATFORM_EMOJI[link.platform] || platform?.name.charAt(0) || '?'} + + )} + + {/* Info */} {platform?.name || link.platform} - {link.username} + {link.username} - + + {/* Action Button */} + {state === 'loading' ? ( ) : ( - - {getButtonLabel(link)} - + {getButtonLabel(link)} )} + ); })} @@ -332,6 +480,7 @@ export default function DevCardViewScreen({ navigation, route }: Props) { {/* Footer */} + Powered by DevCard ⚡ @@ -344,159 +493,139 @@ const styles = StyleSheet.create({ closeBtn: { position: 'absolute', top: 50, right: 20, zIndex: 10, width: 36, height: 36, borderRadius: 18, - backgroundColor: COLORS.bgElevated, alignItems: 'center', justifyContent: 'center', + backgroundColor: 'rgba(255,255,255,0.08)', + borderWidth: 1, borderColor: 'rgba(255,255,255,0.12)', + alignItems: 'center', justifyContent: 'center', }, closeBtnText: { color: COLORS.textSecondary, fontSize: FONT_SIZE.md }, + saveContactBtn: { + position: 'absolute', top: 50, left: 20, zIndex: 10, + paddingHorizontal: SPACING.md, paddingVertical: 8, borderRadius: 18, + backgroundColor: COLORS.primary, + alignItems: 'center', justifyContent: 'center', + }, + saveContactBtnText: { color: COLORS.white, fontSize: FONT_SIZE.sm, fontWeight: '700' }, scrollContent: { padding: SPACING.lg, paddingTop: SPACING.xxl }, premiumHeaderCard: { - backgroundColor: '#0F172A', - borderRadius: 24, - padding: SPACING.xl, + backgroundColor: '#0B1120', + borderRadius: 20, + padding: SPACING.lg, borderWidth: 1, ...SHADOWS.card, marginBottom: SPACING.xl, position: 'relative', overflow: 'hidden', - aspectRatio: 1.58, - justifyContent: 'space-between', + gap: SPACING.md, + }, + cardGlowTop: { + position: 'absolute', + top: -40, + left: -40, + width: 160, + height: 160, + borderRadius: 80, + backgroundColor: 'rgba(99,102,241,0.12)', }, cardGlass: { ...StyleSheet.absoluteFillObject, - backgroundColor: 'rgba(255, 255, 255, 0.03)', + backgroundColor: 'rgba(255,255,255,0.015)', }, cardTop: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - brandRow: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, - miniChip: { - width: 30, - height: 20, - borderRadius: 4, - backgroundColor: '#94A3B8', - opacity: 0.5, - }, - brandText: { - color: 'rgba(255,255,255,0.5)', - fontSize: 10, - fontWeight: '800', - letterSpacing: 2, - }, - contactless: { - fontSize: 20, - opacity: 0.4, - }, - cardMid: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING.lg, + flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, - avatarContainer: { - ...SHADOWS.card, - shadowOpacity: 0.3, - }, - avatar: { - width: 70, - height: 70, - borderRadius: 35, + brandRow: { flexDirection: 'row', alignItems: 'center', gap: 7 }, + miniChip: { width: 28, height: 18, borderRadius: 4, opacity: 0.7 }, + brandText: { color: 'rgba(255,255,255,0.45)', fontSize: 9, fontWeight: '800', letterSpacing: 2.5 }, + cardMid: { flexDirection: 'row', alignItems: 'center', gap: SPACING.md }, + avatarRing: { + borderRadius: 38, borderWidth: 2, - borderColor: 'rgba(255,255,255,0.1)', - }, - avatarPlaceholder: { - alignItems: 'center', - justifyContent: 'center', - }, - avatarText: { - fontSize: 32, - fontWeight: '800', - color: COLORS.white, - }, - mainInfo: { - flex: 1, + padding: 2, }, + avatar: { width: 64, height: 64, borderRadius: 32 }, + avatarPlaceholder: { alignItems: 'center', justifyContent: 'center' }, + avatarText: { fontSize: 28, fontWeight: '800', color: COLORS.white }, + mainInfo: { flex: 1, gap: 3 }, profileName: { - fontSize: 24, - fontWeight: '800', - color: COLORS.white, - letterSpacing: 0.5, + fontSize: 20, fontWeight: '800', color: COLORS.white, letterSpacing: 0.2, }, profileRole: { - fontSize: 12, - color: COLORS.textSecondary, - fontWeight: '600', - marginTop: 2, + fontSize: 11, color: 'rgba(255,255,255,0.55)', fontWeight: '500', lineHeight: 15, }, - pronouns: { - fontSize: 10, - color: COLORS.textMuted, - marginTop: 4, - fontStyle: 'italic', - }, - cardBottom: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - bioContainer: { - flex: 1, - marginRight: SPACING.md, - }, - bioText: { - fontSize: 10, - color: 'rgba(255,255,255,0.4)', - lineHeight: 14, + pronouns: { fontSize: 10, color: COLORS.textMuted, fontStyle: 'italic' }, + cardBottom: { gap: SPACING.xs }, + cardDivider: { + height: 1, backgroundColor: 'rgba(255,255,255,0.06)', marginBottom: 2, }, + bioText: { fontSize: 10.5, color: 'rgba(255,255,255,0.38)', lineHeight: 15 }, cardBadge: { - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 4, - backgroundColor: 'rgba(255,255,255,0.05)', - borderWidth: 0.5, - borderColor: 'rgba(255,255,255,0.1)', - }, - badgeText: { - fontSize: 8, - fontWeight: '900', - color: 'rgba(255,255,255,0.6)', - letterSpacing: 1.5, + alignSelf: 'flex-start', + paddingHorizontal: 8, paddingVertical: 3, borderRadius: 4, + borderWidth: 1, }, + badgeText: { fontSize: 8, fontWeight: '900', letterSpacing: 1.5 }, + + // ─── Tiles ─── tilesSection: { gap: SPACING.sm }, + tilesHeader: { + flexDirection: 'row', alignItems: 'center', + justifyContent: 'space-between', marginBottom: SPACING.xs, + }, tilesLabel: { - fontSize: FONT_SIZE.sm, color: COLORS.textMuted, fontWeight: '600', - textTransform: 'uppercase', letterSpacing: 1, marginBottom: SPACING.xs, + fontSize: FONT_SIZE.xs, color: COLORS.textMuted, fontWeight: '700', + textTransform: 'uppercase', letterSpacing: 1.5, }, + tilesCount: { + backgroundColor: 'rgba(255,255,255,0.08)', + borderRadius: 10, paddingHorizontal: 8, paddingVertical: 2, + borderWidth: 1, borderColor: 'rgba(255,255,255,0.1)', + }, + tilesCountText: { fontSize: 11, fontWeight: '700', color: COLORS.textMuted }, platformTile: { flexDirection: 'row', alignItems: 'center', backgroundColor: COLORS.bgCard, borderRadius: BORDER_RADIUS.md, padding: SPACING.md, borderWidth: 1, borderColor: COLORS.border, + gap: SPACING.sm, + }, + tileDone: { + borderColor: COLORS.success + '55', + backgroundColor: 'rgba(34, 197, 94, 0.06)', }, - tileDone: { borderColor: COLORS.success, backgroundColor: 'rgba(34, 197, 94, 0.05)' }, tileIcon: { - width: 40, height: 40, borderRadius: 10, + width: 44, height: 44, borderRadius: 12, alignItems: 'center', justifyContent: 'center', }, - tileIconText: { color: COLORS.white, fontWeight: '700', fontSize: FONT_SIZE.md }, - tileInfo: { flex: 1, marginLeft: SPACING.md }, + tileIconBorder: { borderWidth: 1 }, + tileIconText: { fontWeight: '800', fontSize: 16, letterSpacing: -0.5 }, + tileIconDoneText: { fontWeight: '800', fontSize: 18, color: COLORS.success }, + tileInfo: { flex: 1 }, tilePlatform: { fontSize: FONT_SIZE.md, fontWeight: '600', color: COLORS.textPrimary }, tileUsername: { fontSize: FONT_SIZE.sm, color: COLORS.textMuted, marginTop: 1 }, tileAction: { - backgroundColor: COLORS.primary, borderRadius: BORDER_RADIUS.sm, - paddingHorizontal: SPACING.md, paddingVertical: SPACING.xs, - minWidth: 72, alignItems: 'center', + borderRadius: BORDER_RADIUS.sm, + paddingHorizontal: SPACING.md, paddingVertical: 7, + minWidth: 72, alignItems: 'center', justifyContent: 'center', }, - tileActionDone: { backgroundColor: COLORS.success }, - tileActionLoading: { backgroundColor: COLORS.primaryDark }, - tileActionText: { color: COLORS.white, fontWeight: '700', fontSize: FONT_SIZE.sm }, - tileActionTextDone: {}, + tileActionText: { color: COLORS.white, fontWeight: '700', fontSize: 13 }, + emptyLinksCard: { + backgroundColor: COLORS.bgCard, + borderRadius: BORDER_RADIUS.md, + borderWidth: 1, + borderColor: COLORS.border, + }, + skelMb8: { marginBottom: 8 }, + skelMb12: { marginBottom: 12 }, + skelMb6: { marginBottom: 6 }, + tileInfoMl16: { marginLeft: 16 }, + + // ─── Error / Footer ─── errorState: { flex: 1, alignItems: 'center', justifyContent: 'center' }, errorEmoji: { fontSize: 48, marginBottom: SPACING.md }, errorText: { fontSize: FONT_SIZE.lg, color: COLORS.textPrimary, fontWeight: '600' }, backLink: { color: COLORS.primary, fontSize: FONT_SIZE.md, marginTop: SPACING.md }, footer: { alignItems: 'center', paddingVertical: SPACING.xl }, - footerText: { fontSize: FONT_SIZE.xs, color: COLORS.textMuted }, + footerDivider: { + width: 40, height: 1, backgroundColor: 'rgba(255,255,255,0.08)', marginBottom: SPACING.md, + }, + footerText: { fontSize: FONT_SIZE.xs, color: COLORS.textMuted, letterSpacing: 0.5 }, }); diff --git a/apps/mobile/src/screens/EventDetailScreen.tsx b/apps/mobile/src/screens/EventDetailScreen.tsx new file mode 100644 index 00000000..3b5e2428 --- /dev/null +++ b/apps/mobile/src/screens/EventDetailScreen.tsx @@ -0,0 +1,184 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + View, Text, StyleSheet, FlatList, TouchableOpacity, + StatusBar, Alert, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; +import Avatar from '../components/Avatar'; +import { LoadingPlaceholder } from '../components/LoadingPlaceholder'; +import { EmptyState } from '../components/EmptyState'; +import { useAuth } from '../context/AuthContext'; +import { get, post, del } from '../services/api'; +import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS, SHADOWS } from '../theme/tokens'; +import type { NativeStackScreenProps } from '@react-navigation/native-stack'; +import type { RootStackParamList } from '../navigation/MainTabs'; + +type Props = NativeStackScreenProps; + +interface Attendee { + id: string; username: string; displayName: string; + bio: string | null; avatarUrl: string | null; accentColor: string; +} + +export default function EventDetailScreen({ route, navigation }: Props) { + const { slug, name } = route.params; + const { token } = useAuth(); + const [event, setEvent] = useState(null); + const [attendees, setAttendees] = useState([]); + const [loading, setLoading] = useState(true); + const [joining, setJoining] = useState(false); + + const fetchEvent = useCallback(async () => { + setLoading(true); + try { + const [detail, atts] = await Promise.all([ + get(`/api/events/${slug}`, token), + get(`/api/events/${slug}/attendees`, token), + ]); + setEvent(detail); + setAttendees(atts?.attendees || []); + } catch { Alert.alert('Error', 'Failed to load event'); } + finally { setLoading(false); } + }, [slug, token]); + + useEffect(() => { fetchEvent(); }, [fetchEvent]); + + const handleJoin = async () => { + setJoining(true); + try { + await post(`/api/events/${slug}/join`, undefined, token); + Alert.alert('Joined!', 'You are now part of this event.'); + fetchEvent(); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : ''; + Alert.alert(msg.includes('409') ? 'Already Joined' : 'Error', + msg.includes('409') ? 'You are already part of this event.' : 'Failed to join.'); + } finally { setJoining(false); } + }; + + const handleLeave = () => { + Alert.alert('Leave Event', 'Are you sure?', [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Leave', style: 'destructive', onPress: async () => { + try { await del(`/api/events/${slug}/leave`, undefined, token); fetchEvent(); } + catch { Alert.alert('Error', 'Failed to leave event'); } + }}, + ]); + }; + + const fmtDate = (s: string) => new Date(s).toLocaleDateString(undefined, { + weekday: 'short', month: 'short', day: 'numeric', + }); + + if (loading) return ( + + + + + ); + + return ( + + + item.id} + contentContainerStyle={styles.list} + ListHeaderComponent={ + + + {event?.name || name} + {event?.location && ( + + + {event.location} + + )} + + + + {event ? `${fmtDate(event.startDate)} – ${fmtDate(event.endDate)}` : ''} + + + {event?.description && ( + {event.description} + )} + + + + {joining ? 'Joining…' : 'Join Event'} + + + + Leave + + + + + Attendees ({event?.attendeesCount || attendees.length}) + + + } + renderItem={({ item }) => ( + navigation.navigate('DevCardView', { username: item.username })} + activeOpacity={0.7}> + + + {item.displayName} + @{item.username} + + + + )} + ListEmptyComponent={ + + } + /> + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: COLORS.bgPrimary }, + list: { padding: SPACING.lg }, + infoCard: { + backgroundColor: COLORS.bgCard, borderRadius: BORDER_RADIUS.lg, + padding: SPACING.lg, borderWidth: 1, borderColor: COLORS.border, + marginBottom: SPACING.lg, ...SHADOWS.card, + }, + eventName: { fontSize: FONT_SIZE.xl, fontWeight: '800', color: COLORS.textPrimary, marginBottom: SPACING.sm }, + metaRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.xs, marginBottom: 4 }, + metaText: { fontSize: FONT_SIZE.sm, color: COLORS.textSecondary }, + description: { fontSize: FONT_SIZE.sm, color: COLORS.textSecondary, marginTop: SPACING.sm, lineHeight: 20 }, + actions: { flexDirection: 'row', gap: SPACING.sm, marginTop: SPACING.lg }, + joinBtn: { + flex: 1, backgroundColor: COLORS.primary, borderRadius: BORDER_RADIUS.md, + padding: SPACING.md, alignItems: 'center', ...SHADOWS.button, + }, + joinBtnText: { color: COLORS.white, fontWeight: '700', fontSize: FONT_SIZE.md }, + leaveBtn: { + backgroundColor: COLORS.bgElevated, borderRadius: BORDER_RADIUS.md, + padding: SPACING.md, paddingHorizontal: SPACING.lg, + borderWidth: 1, borderColor: COLORS.border, + }, + leaveBtnText: { color: COLORS.error, fontWeight: '600', fontSize: FONT_SIZE.md }, + sectionTitle: { + fontSize: FONT_SIZE.lg, fontWeight: '700', color: COLORS.textPrimary, + marginBottom: SPACING.md, + }, + attendeeRow: { + flexDirection: 'row', alignItems: 'center', backgroundColor: COLORS.bgCard, + borderRadius: BORDER_RADIUS.md, padding: SPACING.md, marginBottom: SPACING.sm, + borderWidth: 1, borderColor: COLORS.border, + }, + avatar: { width: 40, height: 40, borderRadius: 20, marginRight: SPACING.md }, + attendeeInfo: { flex: 1 }, + attendeeName: { fontSize: FONT_SIZE.md, fontWeight: '600', color: COLORS.textPrimary }, + attendeeUser: { fontSize: FONT_SIZE.sm, color: COLORS.textMuted, marginTop: 1 }, +}); diff --git a/apps/mobile/src/screens/EventsScreen.tsx b/apps/mobile/src/screens/EventsScreen.tsx new file mode 100644 index 00000000..c4dbf7bf --- /dev/null +++ b/apps/mobile/src/screens/EventsScreen.tsx @@ -0,0 +1,75 @@ +import React, { useState, useCallback } from 'react'; +import { + View, Text, StyleSheet, TextInput, TouchableOpacity, + StatusBar, Alert, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; +import { EmptyState } from '../components/EmptyState'; +import { useAuth } from '../context/AuthContext'; +import { get } from '../services/api'; +import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS, SHADOWS } from '../theme/tokens'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import type { RootStackParamList } from '../navigation/MainTabs'; + +type Props = { navigation: NativeStackNavigationProp }; + +export default function EventsScreen({ navigation }: Props) { + const { token } = useAuth(); + const [slugInput, setSlugInput] = useState(''); + const [loading, setLoading] = useState(false); + + const handleLookup = async () => { + const slug = slugInput.trim().toLowerCase(); + if (!slug) { Alert.alert('Enter a slug', 'Please enter the event slug or code.'); return; } + setLoading(true); + try { + const event = await get(`/api/events/${slug}`, token); + if (event) navigation.navigate('EventDetail', { slug: event.slug, name: event.name }); + } catch { Alert.alert('Not Found', 'No event found with that code.'); } + finally { setLoading(false); setSlugInput(''); } + }; + + return ( + + + + Events + Join an event to network with attendees + + + + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: COLORS.bgPrimary }, + header: { padding: SPACING.lg, paddingBottom: SPACING.sm }, + title: { fontSize: FONT_SIZE.xl, fontWeight: '800', color: COLORS.textPrimary }, + subtitle: { fontSize: FONT_SIZE.sm, color: COLORS.textSecondary, marginTop: SPACING.xs }, + joinSection: { paddingHorizontal: SPACING.lg, paddingBottom: SPACING.lg }, + inputRow: { flexDirection: 'row', gap: SPACING.sm }, + input: { + flex: 1, backgroundColor: COLORS.bgCard, borderRadius: BORDER_RADIUS.md, + padding: SPACING.md, color: COLORS.textPrimary, fontSize: FONT_SIZE.md, + borderWidth: 1, borderColor: COLORS.border, + }, + searchBtn: { + backgroundColor: COLORS.primary, borderRadius: BORDER_RADIUS.md, + width: 48, alignItems: 'center', justifyContent: 'center', ...SHADOWS.button, + }, + disabled: { opacity: 0.5 }, +}); diff --git a/apps/mobile/src/screens/HomeScreen.tsx b/apps/mobile/src/screens/HomeScreen.tsx index 80de203c..b4d504b2 100644 --- a/apps/mobile/src/screens/HomeScreen.tsx +++ b/apps/mobile/src/screens/HomeScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { View, Text, @@ -7,15 +7,18 @@ import { TouchableOpacity, Share, StatusBar, - Image, RefreshControl, + TextInput, } from 'react-native'; +import { Skeleton } from '../components/Skeleton'; +import Avatar from '../components/Avatar'; import { SafeAreaView } from 'react-native-safe-area-context'; import QRCode from 'react-native-qrcode-svg'; import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS, SHADOWS } from '../theme/tokens'; import { useAuth } from '../context/AuthContext'; import { PLATFORMS } from '@devcard/shared'; -import { APP_URL, API_BASE_URL } from '../config'; +import { APP_URL } from '../config'; +import { get } from '../services/api'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RootStackParamList } from '../navigation/MainTabs'; @@ -37,37 +40,37 @@ export default function HomeScreen({ navigation }: Props) { const [analytics, setAnalytics] = useState(null); const [showQR, setShowQR] = useState(false); const [refreshing, setRefreshing] = useState(false); + const [loading, setLoading] = useState(true); + const [searchUsername, setSearchUsername] = useState(''); const profileUrl = user?.defaultCardId ? `${APP_URL}/devcard/${user.defaultCardId}` : `${APP_URL}/u/${user?.username}`; - useEffect(() => { - fetchData(); - }, []); - - const fetchData = async () => { + const fetchData = useCallback(async () => { + setLoading(true); try { - const [profileRes, analyticsRes] = await Promise.all([ - fetch(`${API_BASE_URL}/api/profiles/me`, { - headers: { Authorization: `Bearer ${token}` }, - }), - fetch(`${API_BASE_URL}/api/analytics/overview`, { - headers: { Authorization: `Bearer ${token}` }, - }) + const [profileData, analyticsData] = await Promise.all([ + get('/api/profiles/me', token).catch(() => null), + get('/api/analytics/overview', token).catch(() => null), ]); - if (profileRes.ok) { - const data = await profileRes.json(); - setLinks(data.platformLinks || []); + if (profileData) { + setLinks(profileData.platformLinks || []); } - if (analyticsRes.ok) { - setAnalytics(await analyticsRes.json()); + if (analyticsData) { + setAnalytics(analyticsData); } - } catch (err) { - console.error('Failed to fetch dashboard data:', err); + } catch (error) { + console.error('Failed to fetch dashboard data:', error); + } finally { + setLoading(false); } - }; + }, [token]); + + useEffect(() => { + fetchData(); + }, [fetchData]); const onRefresh = async () => { setRefreshing(true); @@ -81,11 +84,26 @@ export default function HomeScreen({ navigation }: Props) { message: `Check out my DevCard: ${profileUrl}`, url: profileUrl, }); - } catch (err) { - console.error('Share failed:', err); + } catch (error) { + console.error('Share failed:', error); } }; + if (loading) { + return ( + + + + + + + + + + + ); + } + return ( @@ -108,15 +126,7 @@ export default function HomeScreen({ navigation }: Props) { {/* Profile Card Preview */} - {user?.avatarUrl ? ( - - ) : ( - - - {(user?.displayName || 'D').charAt(0).toUpperCase()} - - - )} + {user?.displayName} {user?.pronouns && ( @@ -135,20 +145,26 @@ export default function HomeScreen({ navigation }: Props) { {/* Platform Links Summary */} - {links.slice(0, 4).map(link => { - const platform = PLATFORMS[link.platform]; - return ( - - - {platform?.name || link.platform} - - - ); - })} - {links.length > 4 && ( - - +{links.length - 4} - + {links.length > 0 ? ( + <> + {links.slice(0, 4).map(link => { + const platform = PLATFORMS[link.platform]; + return ( + + + {platform?.name || link.platform} + + + ); + })} + {links.length > 4 && ( + + +{links.length - 4} + + )} + + ) : ( + No platform links added yet. Add links in the Links tab to populate your preview. )} @@ -177,13 +193,13 @@ export default function HomeScreen({ navigation }: Props) { {/* Action Buttons */} - + 📤 - Share Card + Share (navigation as any).navigate('Views')} activeOpacity={0.85}> 📈 - Analytics + Stats 👁️ Preview + + (navigation as any).navigate('Links')} + activeOpacity={0.85}> + 🔗 + Links + + + + + (navigation as any).navigate('Events')} + activeOpacity={0.85}> + 🎪 + Events + + + (navigation as any).navigate('Teams')} + activeOpacity={0.85}> + 👥 + Teams + + + (navigation as any).navigate('Nfc')} + activeOpacity={0.85}> + 📳 + NFC + + + + + {/* Search / Lookup */} + + 🔍 View a DevCard + + { + const u = searchUsername.trim(); + if (u) (navigation as any).navigate('DevCardView', { username: u }); + }} + /> + { + const u = searchUsername.trim(); + if (u) (navigation as any).navigate('DevCardView', { username: u }); + }} + > + Go → + + {/* Stats */} @@ -275,12 +356,13 @@ const styles = StyleSheet.create({ qrToggle: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm }, qrToggleEmoji: { fontSize: 24 }, qrToggleText: { fontSize: FONT_SIZE.md, color: COLORS.textSecondary, fontWeight: '500' }, - actions: { flexDirection: 'row', gap: SPACING.md, marginBottom: SPACING.lg }, + actionsGrid: { flexDirection: 'row', gap: SPACING.sm, marginBottom: SPACING.sm }, actionButton: { flex: 1, backgroundColor: COLORS.bgCard, borderRadius: BORDER_RADIUS.md, - padding: SPACING.md, + padding: SPACING.sm, + paddingVertical: SPACING.md, alignItems: 'center', borderWidth: 1, borderColor: COLORS.border, @@ -299,4 +381,48 @@ const styles = StyleSheet.create({ statNumber: { fontSize: FONT_SIZE.xl, fontWeight: '800', color: COLORS.primary }, statLabel: { fontSize: FONT_SIZE.xs, color: COLORS.textMuted, marginTop: 4 }, statDivider: { width: 1, backgroundColor: COLORS.border }, + loadingRoot: { + flex: 1, + padding: SPACING.lg, + backgroundColor: COLORS.bgPrimary, + }, + loadingSpacer: { + marginTop: SPACING.sm, + }, + loadingSection: { + marginTop: SPACING.lg, + }, + emptyHint: { + color: COLORS.textMuted, + fontSize: FONT_SIZE.sm, + lineHeight: 20, + marginTop: SPACING.sm, + maxWidth: '70%', + }, + // Search + searchSection: { + marginBottom: SPACING.lg, + }, + searchLabel: { + fontSize: FONT_SIZE.sm, fontWeight: '700', color: COLORS.textSecondary, + marginBottom: SPACING.sm, letterSpacing: 0.3, + }, + searchRow: { + flexDirection: 'row', gap: SPACING.sm, + }, + searchInput: { + flex: 1, + backgroundColor: COLORS.bgCard, + borderRadius: BORDER_RADIUS.md, + paddingHorizontal: SPACING.md, paddingVertical: 12, + color: COLORS.textPrimary, fontSize: FONT_SIZE.md, + borderWidth: 1, borderColor: COLORS.border, + }, + searchBtn: { + backgroundColor: COLORS.primary, + borderRadius: BORDER_RADIUS.md, + paddingHorizontal: SPACING.lg, + justifyContent: 'center', alignItems: 'center', + }, + searchBtnText: { color: COLORS.white, fontWeight: '700', fontSize: FONT_SIZE.md }, }); diff --git a/apps/mobile/src/screens/LinksScreen.tsx b/apps/mobile/src/screens/LinksScreen.tsx index daded55f..fd420275 100644 --- a/apps/mobile/src/screens/LinksScreen.tsx +++ b/apps/mobile/src/screens/LinksScreen.tsx @@ -14,8 +14,11 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS } from '../theme/tokens'; import { useAuth } from '../context/AuthContext'; import { PLATFORMS, getAllPlatforms } from '@devcard/shared'; -import { API_BASE_URL } from '../config'; +import { get, post, del } from '../services/api'; +import { EmptyState } from '../components/EmptyState'; +import { LoadingPlaceholder } from '../components/LoadingPlaceholder'; import type { PlatformDef } from '@devcard/shared'; +import DraggableFlatList, { ScaleDecorator, RenderItemParams } from 'react-native-draggable-flatlist'; interface PlatformLink { id: string; @@ -31,18 +34,17 @@ export default function LinksScreen() { const [showAddModal, setShowAddModal] = useState(false); const [selectedPlatform, setSelectedPlatform] = useState(null); const [usernameInput, setUsernameInput] = useState(''); + const [loading, setLoading] = useState(true); const fetchLinks = useCallback(async () => { + setLoading(true); try { - const res = await fetch(`${API_BASE_URL}/api/profiles/me`, { - headers: { Authorization: `Bearer ${token}` }, - }); - if (res.ok) { - const data = await res.json(); - setLinks(data.platformLinks || []); - } - } catch (err) { - console.error('Failed to fetch links:', err); + const data = await get('/api/profiles/me', token).catch(() => null); + setLinks(data?.platformLinks || []); + } catch (error) { + console.error('Failed to fetch links:', error); + } finally { + setLoading(false); } }, [token]); @@ -53,24 +55,12 @@ export default function LinksScreen() { const addLink = async () => { if (!selectedPlatform || !usernameInput.trim()) return; try { - const res = await fetch(`${API_BASE_URL}/api/profiles/me/links`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - platform: selectedPlatform.id, - username: usernameInput.trim(), - }), - }); - if (res.ok) { - setShowAddModal(false); - setSelectedPlatform(null); - setUsernameInput(''); - fetchLinks(); - } - } catch (err) { + await post('/api/profiles/me/links', { platform: selectedPlatform.id, username: usernameInput.trim() }, token); + setShowAddModal(false); + setSelectedPlatform(null); + setUsernameInput(''); + fetchLinks(); + } catch { Alert.alert('Error', 'Failed to add link'); } }; @@ -82,20 +72,71 @@ export default function LinksScreen() { text: 'Remove', style: 'destructive', onPress: async () => { - try { - await fetch(`${API_BASE_URL}/api/profiles/me/links/${id}`, { - method: 'DELETE', - headers: { Authorization: `Bearer ${token}` }, - }); - fetchLinks(); - } catch (err) { - Alert.alert('Error', 'Failed to remove link'); - } + try { + await del(`/api/profiles/me/links/${id}`, undefined, token); + fetchLinks(); + } catch { + Alert.alert('Error', 'Failed to remove link'); + } }, }, ]); }; + const handleReorder = async (data: PlatformLink[]) => { + setLinks(data); + try { + const payload = { + links: data.map((link, index) => ({ id: link.id, displayOrder: index })), + }; + await put('/api/profiles/me/links/reorder', payload, token); + } catch { + Alert.alert('Error', 'Failed to save new order'); + fetchLinks(); // Revert on failure + } + }; + + const renderItem = ({ item, drag, isActive }: RenderItemParams) => { + const platform = PLATFORMS[item.platform]; + return ( + + + + ⋮⋮ + + + + {platform?.name || item.platform} + {item.username} + + deleteLink(item.id)} + style={styles.deleteBtn}> + + + + + ); + }; + + if (loading) { + return ( + + + + + ); + } + return ( @@ -109,33 +150,18 @@ export default function LinksScreen() { - handleReorder(data)} keyExtractor={item => item.id} contentContainerStyle={styles.list} - renderItem={({ item }) => { - const platform = PLATFORMS[item.platform]; - return ( - - - - {platform?.name || item.platform} - {item.username} - - deleteLink(item.id)} - style={styles.deleteBtn}> - - - - ); - }} + renderItem={renderItem} ListEmptyComponent={ - - 🔗 - No links yet - Add your first platform link - + } /> @@ -212,16 +238,30 @@ const styles = StyleSheet.create({ backgroundColor: COLORS.bgCard, borderRadius: BORDER_RADIUS.md, padding: SPACING.md, borderWidth: 1, borderColor: COLORS.border, }, + linkItemActive: { + backgroundColor: COLORS.bgElevated, + borderColor: COLORS.primary, + elevation: 8, + shadowColor: COLORS.black, + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + }, + dragHandle: { + paddingRight: SPACING.sm, + justifyContent: 'center', + }, + dragHandleText: { + color: COLORS.textMuted, + fontSize: 20, + fontWeight: 'bold', + }, platformDot: { width: 12, height: 12, borderRadius: 6, marginRight: SPACING.md }, linkInfo: { flex: 1 }, platformName: { fontSize: FONT_SIZE.md, fontWeight: '600', color: COLORS.textPrimary }, username: { fontSize: FONT_SIZE.sm, color: COLORS.textSecondary, marginTop: 2 }, deleteBtn: { padding: SPACING.sm }, deleteBtnText: { color: COLORS.error, fontSize: FONT_SIZE.md, fontWeight: '700' }, - empty: { alignItems: 'center', paddingVertical: SPACING.xxl }, - emptyEmoji: { fontSize: 48, marginBottom: SPACING.md }, - emptyText: { fontSize: FONT_SIZE.lg, fontWeight: '600', color: COLORS.textPrimary }, - emptySubtext: { fontSize: FONT_SIZE.sm, color: COLORS.textMuted, marginTop: SPACING.xs }, modalOverlay: { flex: 1, backgroundColor: COLORS.overlay, justifyContent: 'flex-end', diff --git a/apps/mobile/src/screens/NfcScreen.tsx b/apps/mobile/src/screens/NfcScreen.tsx new file mode 100644 index 00000000..d14c317d --- /dev/null +++ b/apps/mobile/src/screens/NfcScreen.tsx @@ -0,0 +1,157 @@ +import React, { useState, useCallback } from 'react'; +import { + View, Text, StyleSheet, TouchableOpacity, StatusBar, Alert, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; +import { useAuth } from '../context/AuthContext'; +import { get } from '../services/api'; +import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS, SHADOWS } from '../theme/tokens'; +import type { NfcPayload } from '../types'; + +/** + * NfcScreen — NFC tag write/read UI. + * + * NOTE: Actual NFC hardware interaction requires `react-native-nfc-manager` + * which needs a dev build (not Expo Go). This screen provides the UI and + * fetches the NDEF payload from the backend. The NFC write call is stubbed + * with a TODO for native module integration. + */ +export default function NfcScreen() { + const { token } = useAuth(); + const [payload, setPayload] = useState(null); + const [loading, setLoading] = useState(false); + const [written, setWritten] = useState(false); + + const fetchPayload = useCallback(async () => { + setLoading(true); + try { + const data = await get('/api/nfc/payload', token); + setPayload(data); + } catch { + Alert.alert('Error', 'Failed to fetch NFC payload from server.'); + } finally { + setLoading(false); + } + }, [token]); + + const handleWriteTag = async () => { + if (!payload) { + await fetchPayload(); + return; + } + + // TODO: Integrate react-native-nfc-manager here + // import NfcManager, { NfcTech, Ndef } from 'react-native-nfc-manager'; + // await NfcManager.requestTechnology(NfcTech.Ndef); + // const bytes = Ndef.encodeMessage([Ndef.uriRecord(payload.payload)]); + // await NfcManager.ndefHandler.writeNdefMessage(bytes); + // await NfcManager.cancelTechnologyRequest(); + + Alert.alert( + 'NFC Not Available', + 'NFC write requires a dev build with react-native-nfc-manager. The payload URL has been prepared.', + [{ text: 'OK' }], + ); + setWritten(false); + }; + + return ( + + + + + + + + + NFC Tag Writer + + Write your DevCard URL to an NFC tag so anyone can tap to view your profile. + + + + + + Payload URL + + + {payload?.payload || 'Tap "Prepare" to generate'} + + + + + + + {loading ? 'Loading…' : 'Prepare Payload'} + + + + + + + Write to NFC Tag + + + + {written && ( + + + Tag written successfully! + + )} + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: COLORS.bgPrimary }, + content: { flex: 1, padding: SPACING.lg, alignItems: 'center', justifyContent: 'center' }, + iconContainer: { + width: 120, height: 120, borderRadius: 60, + backgroundColor: COLORS.bgCard, alignItems: 'center', justifyContent: 'center', + borderWidth: 2, borderColor: COLORS.primary + '44', marginBottom: SPACING.lg, + }, + title: { fontSize: FONT_SIZE.xl, fontWeight: '800', color: COLORS.textPrimary }, + subtitle: { + fontSize: FONT_SIZE.sm, color: COLORS.textSecondary, textAlign: 'center', + marginTop: SPACING.xs, marginBottom: SPACING.xl, lineHeight: 20, maxWidth: 300, + }, + card: { + width: '100%', backgroundColor: COLORS.bgCard, borderRadius: BORDER_RADIUS.lg, + padding: SPACING.lg, borderWidth: 1, borderColor: COLORS.border, marginBottom: SPACING.lg, + }, + cardRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.xs, marginBottom: SPACING.sm }, + cardLabel: { fontSize: FONT_SIZE.sm, color: COLORS.textSecondary, fontWeight: '500' }, + payloadUrl: { fontSize: FONT_SIZE.sm, color: COLORS.primary, fontFamily: 'monospace' }, + prepareBtn: { + flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, + backgroundColor: COLORS.bgElevated, borderRadius: BORDER_RADIUS.md, + padding: SPACING.md, paddingHorizontal: SPACING.lg, marginBottom: SPACING.md, + borderWidth: 1, borderColor: COLORS.border, + }, + prepareBtnText: { color: COLORS.textPrimary, fontWeight: '600', fontSize: FONT_SIZE.md }, + writeBtn: { + flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, + backgroundColor: COLORS.primary, borderRadius: BORDER_RADIUS.md, + padding: SPACING.md, paddingHorizontal: SPACING.xl, ...SHADOWS.button, + }, + writeBtnDisabled: { backgroundColor: COLORS.bgElevated }, + writeBtnText: { color: COLORS.white, fontWeight: '700', fontSize: FONT_SIZE.md }, + writeBtnTextDisabled: { color: COLORS.textMuted }, + successBanner: { + flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, + marginTop: SPACING.lg, backgroundColor: 'rgba(34,197,94,0.1)', + borderRadius: BORDER_RADIUS.md, padding: SPACING.md, + }, + successText: { color: COLORS.success, fontWeight: '600', fontSize: FONT_SIZE.sm }, +}); diff --git a/apps/mobile/src/screens/ScanScreen.tsx b/apps/mobile/src/screens/ScanScreen.tsx index b864cdd6..1f300351 100644 --- a/apps/mobile/src/screens/ScanScreen.tsx +++ b/apps/mobile/src/screens/ScanScreen.tsx @@ -7,19 +7,26 @@ import { TextInput, StatusBar, Alert, - ActivityIndicator, + Share, + Platform, + PermissionsAndroid, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useFocusEffect } from '@react-navigation/native'; import QRCode from 'react-native-qrcode-svg'; +import ViewShot from 'react-native-view-shot'; +import { Camera } from 'react-native-camera-kit'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS } from '../theme/tokens'; +import { EmptyState } from '../components/EmptyState'; +import { Skeleton } from '../components/Skeleton'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RootStackParamList } from '../navigation/MainTabs'; import type { BottomSheetModal } from '@gorhom/bottom-sheet'; import type { Card } from '@devcard/shared'; import { useAuth } from '../context/AuthContext'; -import { API_BASE_URL, APP_URL } from '../config'; +import { APP_URL } from '../config'; +import { get } from '../services/api'; import CardPickerSheet from '../components/CardPickerSheet'; type Props = { @@ -39,6 +46,9 @@ export default function ScanScreen({ navigation }: Props) { const [loadingCards, setLoadingCards] = useState(false); const sheetRef = useRef(null); + const qrRef = useRef(null); + const [hasPermission, setHasPermission] = useState(false); + // Extract username from DevCard URL const parseDevCardUrl = (url: string): string | null => { const match = url.match(/\/u\/([a-zA-Z0-9_-]+)/); @@ -55,22 +65,59 @@ export default function ScanScreen({ navigation }: Props) { } }; - // NOTE: Camera QR scanning requires react-native-camera-kit - // which needs native setup. For now, we provide manual entry. - // Camera integration will be added when building on device. + const requestCameraPermission = async () => { + if (Platform.OS === 'android') { + try { + const granted = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.CAMERA, + { + title: 'Camera Permission', + message: 'DevCard needs camera access to scan QR codes.', + buttonNeutral: 'Ask Me Later', + buttonNegative: 'Cancel', + buttonPositive: 'OK', + }, + ); + setHasPermission(granted === PermissionsAndroid.RESULTS.GRANTED); + } catch (err) { + console.warn(err); + } + } else { + // iOS permissions would typically be handled via react-native-permissions + // For this demo, assume true if not Android + setHasPermission(true); + } + }; + + const handleCameraRead = (url: string) => { + const username = parseDevCardUrl(url); + if (username) { + navigation.navigate('DevCardView', { username }); + } + }; + + const handleSaveQR = async () => { + if (qrRef.current && qrRef.current.capture) { + try { + const uri = await qrRef.current.capture(); + await Share.share({ + title: 'My DevCard QR', + url: uri, + }); + } catch (err) { + Alert.alert('Error', 'Failed to save QR code'); + } + } + }; const fetchCards = useCallback(async () => { if (!token) return; setLoadingCards(true); try { - const res = await fetch(`${API_BASE_URL}/api/cards`, { - headers: { Authorization: `Bearer ${token}` }, - }); - if (res.ok) { - setCards(await res.json()); - } - } catch (err) { - console.error('Failed to fetch cards:', err); + const data = await get('/api/cards', token).catch(() => []); + setCards(data || []); + } catch (error) { + console.error('Failed to fetch cards:', error); } finally { setLoadingCards(false); } @@ -131,8 +178,8 @@ export default function ScanScreen({ navigation }: Props) { setSelectedCardId(cardId); try { await AsyncStorage.setItem(LAST_SELECTED_CARD_KEY, cardId); - } catch (err) { - console.error('Failed to persist selected card:', err); + } catch (error) { + console.error('Failed to persist selected card:', error); } finally { sheetRef.current?.dismiss(); } @@ -180,9 +227,12 @@ export default function ScanScreen({ navigation }: Props) { - + {loadingCards ? ( - + + + + ) : qrUrl ? ( ) : ( - Create a card to generate a QR + )} - + + {!!qrUrl && ( - Scan to open your DevCard + + Scan to open your DevCard + + Share QR Image + + )} - {/* Camera Placeholder */} + {/* Camera Scanner */} - - 📷 - Camera QR Scanner - - Point your camera at a DevCard QR code - - + {hasPermission ? ( + handleCameraRead(event.nativeEvent.codeStringValue)} + showFrame={false} + /> + ) : ( + + 📷 + Camera Permission Required + + Grant Permission + + + )} {/* Corner markers */} @@ -290,7 +358,22 @@ const styles = StyleSheet.create({ minHeight: 220, }, qrHint: { textAlign: 'center', color: COLORS.textMuted, fontSize: FONT_SIZE.sm }, - qrPlaceholder: { color: COLORS.textMuted, fontSize: FONT_SIZE.sm }, + saveQrBtn: { + backgroundColor: COLORS.bgElevated, + borderRadius: BORDER_RADIUS.sm, + paddingHorizontal: SPACING.md, + paddingVertical: SPACING.xs, + borderWidth: 1, + borderColor: COLORS.border, + }, + saveQrBtnText: { color: COLORS.primary, fontSize: FONT_SIZE.xs, fontWeight: '600' }, + qrFooter: { alignItems: 'center', marginTop: SPACING.sm, gap: SPACING.xs }, + qrSkeleton: { + alignItems: 'center', + }, + qrSkeletonText: { + marginTop: SPACING.md, + }, cameraArea: { flex: 1, maxHeight: 350, backgroundColor: COLORS.bgCard, borderRadius: BORDER_RADIUS.lg, @@ -302,6 +385,12 @@ const styles = StyleSheet.create({ cameraEmoji: { fontSize: 48, marginBottom: SPACING.md }, cameraText: { fontSize: FONT_SIZE.md, fontWeight: '600', color: COLORS.textPrimary }, cameraSubtext: { fontSize: FONT_SIZE.sm, color: COLORS.textMuted, marginTop: SPACING.xs }, + reqPermBtn: { + backgroundColor: COLORS.primary, borderRadius: BORDER_RADIUS.sm, + paddingHorizontal: SPACING.md, paddingVertical: SPACING.sm, + marginTop: SPACING.md, + }, + reqPermBtnText: { color: COLORS.white, fontSize: FONT_SIZE.sm, fontWeight: '600' }, corner: { position: 'absolute', width: 30, height: 30, borderColor: COLORS.primary, borderWidth: 3, diff --git a/apps/mobile/src/screens/SettingsScreen.tsx b/apps/mobile/src/screens/SettingsScreen.tsx index 7d282a63..933d08d9 100644 --- a/apps/mobile/src/screens/SettingsScreen.tsx +++ b/apps/mobile/src/screens/SettingsScreen.tsx @@ -8,46 +8,53 @@ import { TextInput, Alert, StatusBar, - Image, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; +import { useNavigation } from '@react-navigation/native'; import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS } from '../theme/tokens'; +import Avatar from '../components/Avatar'; +import ColorPicker from '../components/ColorPicker'; import { useAuth } from '../context/AuthContext'; -import { API_BASE_URL } from '../config'; +import { put } from '../services/api'; export default function SettingsScreen() { + const navigation = useNavigation(); const { user, token, refreshUser, logout } = useAuth(); const [displayName, setDisplayName] = useState(user?.displayName || ''); const [bio, setBio] = useState(user?.bio || ''); const [pronouns, setPronouns] = useState(user?.pronouns || ''); const [role, setRole] = useState(user?.role || ''); const [company, setCompany] = useState(user?.company || ''); + const [accentColor, setAccentColor] = useState(user?.accentColor || '#6366F1'); const [saving, setSaving] = useState(false); + const handleAvatarTap = () => { + // TODO: Integrate react-native-image-picker when building on device + // import { launchImageLibrary } from 'react-native-image-picker'; + // const result = await launchImageLibrary({ mediaType: 'photo', quality: 0.8 }); + // Upload via multipart/form-data to PUT /api/profiles/me/avatar + Alert.alert( + 'Change Avatar', + 'Avatar upload requires react-native-image-picker in a dev build. Coming soon!', + ); + }; + const handleSave = async () => { setSaving(true); try { - const res = await fetch(`${API_BASE_URL}/api/profiles/me`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - displayName: displayName.trim(), - bio: bio.trim() || null, - pronouns: pronouns.trim() || null, - role: role.trim() || null, - company: company.trim() || null, - }), - }); - if (res.ok) { - await refreshUser(); - Alert.alert('Success', 'Profile updated!'); - } else { - Alert.alert('Error', 'Failed to update profile'); - } - } catch (err) { + const payload = { + displayName: displayName.trim() || undefined, + bio: bio.trim() || null, + pronouns: pronouns.trim() || null, + role: role.trim() || null, + company: company.trim() || null, + accentColor, + }; + + await put('/api/profiles/me', payload, token); + await refreshUser(); + Alert.alert('Success', 'Profile updated!'); + } catch { Alert.alert('Error', 'Something went wrong'); } finally { setSaving(false); @@ -69,17 +76,16 @@ export default function SettingsScreen() { Profile Settings {/* Avatar */} - - {user?.avatarUrl ? ( - - ) : ( - - - {(user?.displayName || 'D').charAt(0).toUpperCase()} - - - )} + + + Tap to change @{user?.username} + + + {/* Accent Color */} + + Card Accent Color + {/* Form */} @@ -163,11 +169,13 @@ const styles = StyleSheet.create({ title: { fontSize: FONT_SIZE.xl, fontWeight: '800', color: COLORS.textPrimary, marginBottom: SPACING.lg }, avatarSection: { alignItems: 'center', marginBottom: SPACING.xl }, avatar: { width: 80, height: 80, borderRadius: 40 }, + avatarHint: { fontSize: FONT_SIZE.xs, color: COLORS.primary, marginTop: SPACING.xs, fontWeight: '500' }, avatarPlaceholder: { backgroundColor: COLORS.primary, alignItems: 'center', justifyContent: 'center', }, avatarText: { fontSize: FONT_SIZE.xxl, fontWeight: '700', color: COLORS.white }, usernameDisplay: { fontSize: FONT_SIZE.md, color: COLORS.textSecondary, marginTop: SPACING.sm }, + colorSection: { marginBottom: SPACING.lg }, form: { gap: SPACING.md, marginBottom: SPACING.lg }, field: {}, fieldLabel: { fontSize: FONT_SIZE.sm, color: COLORS.textSecondary, marginBottom: SPACING.xs, fontWeight: '500' }, diff --git a/apps/mobile/src/screens/SplashScreen.tsx b/apps/mobile/src/screens/SplashScreen.tsx new file mode 100644 index 00000000..2e6c4991 --- /dev/null +++ b/apps/mobile/src/screens/SplashScreen.tsx @@ -0,0 +1,88 @@ +import React, { useEffect, useRef } from 'react'; +import { View, Text, StyleSheet, Animated } from 'react-native'; +import { COLORS, FONT_SIZE, SPACING } from '../theme/tokens'; + +/** + * SplashScreen — Branded loading screen shown during auth token hydration. + * + * Uses a pulsing opacity animation on the logo to indicate loading activity + * without requiring any external dependencies. + */ +export default function SplashScreen() { + const opacity = useRef(new Animated.Value(0.4)).current; + const scale = useRef(new Animated.Value(0.9)).current; + + useEffect(() => { + Animated.loop( + Animated.sequence([ + Animated.parallel([ + Animated.timing(opacity, { + toValue: 1, + duration: 800, + useNativeDriver: true, + }), + Animated.timing(scale, { + toValue: 1.05, + duration: 800, + useNativeDriver: true, + }), + ]), + Animated.parallel([ + Animated.timing(opacity, { + toValue: 0.4, + duration: 800, + useNativeDriver: true, + }), + Animated.timing(scale, { + toValue: 0.9, + duration: 800, + useNativeDriver: true, + }), + ]), + ]), + ).start(); + }, [opacity, scale]); + + return ( + + + + + DevCard + Loading your profile… + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: COLORS.bgPrimary, + alignItems: 'center', + justifyContent: 'center', + gap: SPACING.md, + }, + logoContainer: { + width: 100, + height: 100, + borderRadius: 50, + backgroundColor: COLORS.bgCard, + alignItems: 'center', + justifyContent: 'center', + borderWidth: 2, + borderColor: COLORS.primary + '44', + }, + logo: { + fontSize: 48, + }, + title: { + fontSize: FONT_SIZE.xxl, + fontWeight: '800', + color: COLORS.textPrimary, + letterSpacing: -0.5, + }, + subtitle: { + fontSize: FONT_SIZE.sm, + color: COLORS.textMuted, + }, +}); diff --git a/apps/mobile/src/screens/TeamDetailScreen.tsx b/apps/mobile/src/screens/TeamDetailScreen.tsx new file mode 100644 index 00000000..9503bb72 --- /dev/null +++ b/apps/mobile/src/screens/TeamDetailScreen.tsx @@ -0,0 +1,127 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + View, Text, StyleSheet, FlatList, TouchableOpacity, + StatusBar, Alert, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; +import Avatar from '../components/Avatar'; +import { LoadingPlaceholder } from '../components/LoadingPlaceholder'; +import { EmptyState } from '../components/EmptyState'; +import { useAuth } from '../context/AuthContext'; +import { get } from '../services/api'; +import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS, SHADOWS } from '../theme/tokens'; +import type { NativeStackScreenProps } from '@react-navigation/native-stack'; +import type { RootStackParamList } from '../navigation/MainTabs'; +import type { TeamMember } from '../types'; + +type Props = NativeStackScreenProps; + +export default function TeamDetailScreen({ route, navigation }: Props) { + const { slug, name } = route.params; + const { token } = useAuth(); + const [team, setTeam] = useState(null); + const [loading, setLoading] = useState(true); + + const fetchTeam = useCallback(async () => { + setLoading(true); + try { + const data = await get(`/api/teams/${slug}`, token); + setTeam(data); + } catch { Alert.alert('Error', 'Failed to load team'); } + finally { setLoading(false); } + }, [slug, token]); + + useEffect(() => { fetchTeam(); }, [fetchTeam]); + + const getRoleBadge = (role: string) => { + switch (role) { + case 'OWNER': return { label: 'Owner', color: COLORS.warning }; + case 'ADMIN': return { label: 'Admin', color: COLORS.info }; + default: return { label: 'Member', color: COLORS.textMuted }; + } + }; + + if (loading) return ( + + + + + ); + + const members: TeamMember[] = team?.members || []; + + return ( + + + item.username} + contentContainerStyle={styles.list} + ListHeaderComponent={ + + + {team?.name || name} + {team?.description && ( + {team.description} + )} + + {members.length} member{members.length !== 1 ? 's' : ''} + + + Members + + } + renderItem={({ item }) => { + const badge = getRoleBadge(item.teamRole); + return ( + navigation.navigate('DevCardView', { username: item.username })} + activeOpacity={0.7}> + + + {item.displayName} + {item.role && {item.role}} + + + {badge.label} + + + ); + }} + ListEmptyComponent={ + + } + /> + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: COLORS.bgPrimary }, + list: { padding: SPACING.lg }, + infoCard: { + backgroundColor: COLORS.bgCard, borderRadius: BORDER_RADIUS.lg, + padding: SPACING.lg, borderWidth: 1, borderColor: COLORS.border, + marginBottom: SPACING.lg, ...SHADOWS.card, + }, + teamName: { fontSize: FONT_SIZE.xl, fontWeight: '800', color: COLORS.textPrimary, marginBottom: SPACING.xs }, + description: { fontSize: FONT_SIZE.sm, color: COLORS.textSecondary, lineHeight: 20, marginBottom: SPACING.sm }, + memberCount: { fontSize: FONT_SIZE.sm, color: COLORS.primary, fontWeight: '600' }, + sectionTitle: { fontSize: FONT_SIZE.lg, fontWeight: '700', color: COLORS.textPrimary, marginBottom: SPACING.md }, + memberRow: { + flexDirection: 'row', alignItems: 'center', backgroundColor: COLORS.bgCard, + borderRadius: BORDER_RADIUS.md, padding: SPACING.md, marginBottom: SPACING.sm, + borderWidth: 1, borderColor: COLORS.border, + }, + avatar: { width: 40, height: 40, borderRadius: 20, marginRight: SPACING.md }, + memberInfo: { flex: 1 }, + memberName: { fontSize: FONT_SIZE.md, fontWeight: '600', color: COLORS.textPrimary }, + memberRole: { fontSize: FONT_SIZE.sm, color: COLORS.textMuted, marginTop: 1 }, + roleBadge: { + borderWidth: 1, borderRadius: BORDER_RADIUS.full, + paddingHorizontal: SPACING.sm, paddingVertical: 2, + }, + roleBadgeText: { fontSize: FONT_SIZE.xs, fontWeight: '600' }, +}); diff --git a/apps/mobile/src/screens/TeamsScreen.tsx b/apps/mobile/src/screens/TeamsScreen.tsx new file mode 100644 index 00000000..c64e047e --- /dev/null +++ b/apps/mobile/src/screens/TeamsScreen.tsx @@ -0,0 +1,75 @@ +import React, { useState, useCallback } from 'react'; +import { + View, Text, StyleSheet, FlatList, TouchableOpacity, + TextInput, StatusBar, Alert, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; +import { EmptyState } from '../components/EmptyState'; +import { useAuth } from '../context/AuthContext'; +import { get } from '../services/api'; +import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS, SHADOWS } from '../theme/tokens'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import type { RootStackParamList } from '../navigation/MainTabs'; + +type Props = { navigation: NativeStackNavigationProp }; + +export default function TeamsScreen({ navigation }: Props) { + const { token } = useAuth(); + const [slugInput, setSlugInput] = useState(''); + const [loading, setLoading] = useState(false); + + const handleLookup = async () => { + const slug = slugInput.trim().toLowerCase(); + if (!slug) { Alert.alert('Enter a slug', 'Enter the team slug.'); return; } + setLoading(true); + try { + const team = await get(`/api/teams/${slug}`, token); + if (team) navigation.navigate('TeamDetail', { slug: team.slug, name: team.name }); + } catch { Alert.alert('Not Found', 'No team found with that slug.'); } + finally { setLoading(false); setSlugInput(''); } + }; + + return ( + + + + Teams + Look up a team to view their group DevCard + + + + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: COLORS.bgPrimary }, + header: { padding: SPACING.lg, paddingBottom: SPACING.sm }, + title: { fontSize: FONT_SIZE.xl, fontWeight: '800', color: COLORS.textPrimary }, + subtitle: { fontSize: FONT_SIZE.sm, color: COLORS.textSecondary, marginTop: SPACING.xs }, + joinSection: { paddingHorizontal: SPACING.lg, paddingBottom: SPACING.lg }, + inputRow: { flexDirection: 'row', gap: SPACING.sm }, + input: { + flex: 1, backgroundColor: COLORS.bgCard, borderRadius: BORDER_RADIUS.md, + padding: SPACING.md, color: COLORS.textPrimary, fontSize: FONT_SIZE.md, + borderWidth: 1, borderColor: COLORS.border, + }, + searchBtn: { + backgroundColor: COLORS.primary, borderRadius: BORDER_RADIUS.md, + width: 48, alignItems: 'center', justifyContent: 'center', ...SHADOWS.button, + }, + disabled: { opacity: 0.5 }, +}); diff --git a/apps/mobile/src/screens/ViewsScreen.tsx b/apps/mobile/src/screens/ViewsScreen.tsx index 24dc79ee..cd0654ea 100644 --- a/apps/mobile/src/screens/ViewsScreen.tsx +++ b/apps/mobile/src/screens/ViewsScreen.tsx @@ -1,10 +1,13 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { View, Text, StyleSheet, FlatList, ActivityIndicator, Image } from 'react-native'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { View, Text, StyleSheet, FlatList } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS } from '../theme/tokens'; import { useAuth } from '../context/AuthContext'; -import { API_BASE_URL } from '../config'; +import { get } from '../services/api'; +import { EmptyState } from '../components/EmptyState'; +import Avatar from '../components/Avatar'; +import { LoadingPlaceholder } from '../components/LoadingPlaceholder'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import type { RootStackParamList } from '../navigation/MainTabs'; @@ -14,30 +17,30 @@ export const ViewsScreen: React.FC = () => { const { token } = useAuth(); const [loading, setLoading] = useState(true); const [views, setViews] = useState([]); + const [overview, setOverview] = useState(null); - const fetchViews = useCallback(async () => { + const fetchData = useCallback(async () => { if (!token) { setLoading(false); return; } try { - const response = await fetch(`${API_BASE_URL}/api/analytics/views`, { - headers: { Authorization: `Bearer ${token}` }, - }); - if (response.ok) { - const data = await response.json(); - setViews(data.data || []); - } - } catch (err) { - console.error('Failed to fetch views analytics', err); + const [viewsData, overviewData] = await Promise.all([ + get('/api/analytics/views', token).catch(() => null), + get('/api/analytics/overview', token).catch(() => null), + ]); + setViews(viewsData?.data || []); + setOverview(overviewData); + } catch (error) { + console.error('Failed to fetch analytics', error); } finally { setLoading(false); } }, [token]); useEffect(() => { - fetchViews(); - }, [fetchViews]); + fetchData(); + }, [fetchData]); const formatDate = (dateString: string) => { const d = new Date(dateString); @@ -53,6 +56,59 @@ export const ViewsScreen: React.FC = () => { } }; + // Generate simple bar chart data for last 7 days + const chartData = useMemo(() => { + const last7Days = Array.from({ length: 7 }, (_, i) => { + const d = new Date(); + d.setDate(d.getDate() - (6 - i)); + return { date: d.toLocaleDateString('en-US', { weekday: 'short' }), count: 0 }; + }); + + views.forEach(v => { + const d = new Date(v.createdAt).toLocaleDateString('en-US', { weekday: 'short' }); + const day = last7Days.find(x => x.date === d); + if (day) day.count++; + }); + + const max = Math.max(...last7Days.map(d => d.count), 1); // prevent division by zero + return { data: last7Days, max }; + }, [views]); + + const renderHeader = () => ( + + + + {overview?.totalViews || 0} + Total Views + + + {overview?.followsCount || 0} + Connections + + + + + Views (Last 7 Days) + + {chartData.data.map((item, idx) => { + const heightPerc = (item.count / chartData.max) * 100; + return ( + + {item.count > 0 ? item.count : ''} + + + + {item.date} + + ); + })} + + + + Recent Activity + + ); + const renderItem = ({ item }: { item: any }) => { const isAnonymous = !item.viewer; @@ -64,11 +120,9 @@ export const ViewsScreen: React.FC = () => { ) : item.viewer.avatarUrl ? ( - + ) : ( - - {item.viewer.displayName.charAt(0)} - + )} @@ -92,25 +146,26 @@ export const ViewsScreen: React.FC = () => { if (loading) { return ( - - - + + + ); } return ( {views.length === 0 ? ( - - - No Views Yet - Share your card or QR code to start collecting analytics. - + ) : ( item.id} renderItem={renderItem} + ListHeaderComponent={renderHeader} contentContainerStyle={styles.listContainer} /> )} @@ -214,4 +269,87 @@ const styles = StyleSheet.create({ textAlign: 'center', marginTop: SPACING.sm, }, + headerContainer: { + paddingBottom: SPACING.lg, + }, + statsRow: { + flexDirection: 'row', + gap: SPACING.md, + marginBottom: SPACING.lg, + }, + statCard: { + flex: 1, + backgroundColor: COLORS.bgCard, + borderRadius: BORDER_RADIUS.lg, + padding: SPACING.lg, + alignItems: 'center', + borderWidth: 1, + borderColor: COLORS.borderLight, + }, + statValue: { + fontSize: 28, + fontWeight: '800', + color: COLORS.primary, + }, + statLabel: { + fontSize: FONT_SIZE.sm, + color: COLORS.textMuted, + marginTop: 4, + fontWeight: '600', + }, + chartCard: { + backgroundColor: COLORS.bgCard, + borderRadius: BORDER_RADIUS.lg, + padding: SPACING.lg, + borderWidth: 1, + borderColor: COLORS.borderLight, + marginBottom: SPACING.xl, + }, + chartTitle: { + fontSize: FONT_SIZE.md, + fontWeight: '700', + color: COLORS.textPrimary, + marginBottom: SPACING.lg, + }, + chartContainer: { + flexDirection: 'row', + alignItems: 'flex-end', + justifyContent: 'space-between', + height: 140, + paddingTop: 20, + }, + barWrapper: { + alignItems: 'center', + flex: 1, + }, + barTrack: { + width: 24, + height: 100, + backgroundColor: COLORS.bgElevated, + borderRadius: 4, + justifyContent: 'flex-end', + overflow: 'hidden', + }, + barFill: { + width: '100%', + backgroundColor: COLORS.primary, + borderRadius: 4, + }, + barLabel: { + fontSize: 10, + color: COLORS.textMuted, + marginTop: SPACING.sm, + }, + barLabelTop: { + fontSize: 10, + color: COLORS.primary, + marginBottom: 4, + fontWeight: 'bold', + }, + sectionTitle: { + fontSize: FONT_SIZE.lg, + fontWeight: '700', + color: COLORS.textPrimary, + marginBottom: SPACING.md, + }, }); diff --git a/apps/mobile/src/screens/WebViewScreen.tsx b/apps/mobile/src/screens/WebViewScreen.tsx index 03806d8f..844c248a 100644 --- a/apps/mobile/src/screens/WebViewScreen.tsx +++ b/apps/mobile/src/screens/WebViewScreen.tsx @@ -1,14 +1,19 @@ -import React, { useRef } from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, StatusBar, + Linking, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { WebView } from 'react-native-webview'; -import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS } from '../theme/tokens'; +import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS, SHADOWS } from '../theme/tokens'; +import { Skeleton } from '../components/Skeleton'; +import { getDeepLinkUrl } from '@devcard/shared'; +import { post } from '../services/api'; +import { useAuth } from '../context/AuthContext'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RouteProp } from '@react-navigation/native'; import type { RootStackParamList } from '../navigation/MainTabs'; @@ -23,64 +28,486 @@ type Props = { * * Opens the platform profile in an in-app WebView so the user can * tap the native Follow/Connect button without leaving DevCard. - * - * Key features: - * - sharedCookiesEnabled: shares auth cookies from system browser OAuth - * - Auto-detects when user navigates away (they tapped Connect) - * - Clean close button to dismiss */ export default function WebViewScreen({ navigation, route }: Props) { - const { platform, profileUrl, displayName } = route.params; + const { + platform, + url, + platformName, + username, + linkId, + cardOwnerUsername, + } = route.params; + + const { token } = useAuth(); + const platformDisplayName = platformName || platform; const webViewRef = useRef(null); + const [hasLoaded, setHasLoaded] = useState(false); + const [fallbackTriggered, setFallbackTriggered] = useState(false); + const [showFallbackOverlay, setShowFallbackOverlay] = useState(false); + const [successToast, setSuccessToast] = useState(null); + const [progress, setProgress] = useState(0); + + const isSuccessHandled = useRef(false); + const successTimerRef = useRef | null>(null); + // Track whether the injected JS ever detected success during this session + const successDetectedInSession = useRef(false); + + // Safety Timeout Fallback: 10 seconds + useEffect(() => { + if (hasLoaded || fallbackTriggered) return; + + const timer = setTimeout(() => { + setFallbackTriggered(true); + setShowFallbackOverlay(true); + }, 10000); + + return () => clearTimeout(timer); + }, [hasLoaded, fallbackTriggered]); + + useEffect(() => { + return () => { + if (successTimerRef.current) { + clearTimeout(successTimerRef.current); + } + }; + }, []); + + const handleOpenDeepLink = () => { + let targetUsername = username; + if (!targetUsername && url) { + const parts = url.split('/'); + const lastPart = parts[parts.length - 1] || parts[parts.length - 2]; + targetUsername = lastPart.split('?')[0]; + } + + const deepLink = targetUsername ? getDeepLinkUrl(platform, targetUsername) : null; + if (deepLink) { + Linking.canOpenURL(deepLink) + .then((supported) => { + Linking.openURL(supported ? deepLink : url); + navigation.goBack(); + }) + .catch(() => { + Linking.openURL(url); + navigation.goBack(); + }); + } else { + Linking.openURL(url); + navigation.goBack(); + } + }; + + const handleOpenBrowser = () => { + Linking.openURL(url); + navigation.goBack(); + }; + + const handleRetryWebView = () => { + setHasLoaded(false); + setFallbackTriggered(false); + setShowFallbackOverlay(false); + setProgress(0); + webViewRef.current?.reload(); + }; + + const handleSuccess = async () => { + if (isSuccessHandled.current) return; + isSuccessHandled.current = true; + successDetectedInSession.current = true; + setSuccessToast(`Connection request sent on ${platformDisplayName}`); + + // Asynchronously log follow to the backend + if (token && username) { + try { + await post(`/api/follow/${platform}/${username}/log`, { status: 'success', layer: 'webview' }, token); + } catch (error) { + console.warn('Failed to log WebView follow success:', error); + } + } + + // Auto-dismiss after 2 seconds with success param back to parent + successTimerRef.current = setTimeout(() => { + navigateBackWithSuccess(); + }, 2000); + }; + + const navigateBackWithSuccess = () => { + if (linkId) { + navigation.navigate({ + name: 'DevCardView', + params: { username: cardOwnerUsername, followSuccessLinkId: linkId }, + merge: true, + }); + } else { + navigation.goBack(); + } + }; + + // Done button: check current page state live before going back + const handleDonePress = () => { + // If success was already handled, navigate with success immediately + if (successDetectedInSession.current) { + if (successTimerRef.current) clearTimeout(successTimerRef.current); + navigateBackWithSuccess(); + return; + } + + // Inject a one-shot check script to see if LinkedIn currently shows success + const checkScript = ` + (function() { + var bodyText = document.body ? document.body.innerText.toLowerCase() : ''; + var successKeywords = ['invite sent', 'invitation sent', 'request sent', 'pending']; + var found = successKeywords.some(function(k) { return bodyText.includes(k); }); + if (!found) { + var els = document.querySelectorAll('button, a, span, [role="button"]'); + for (var i = 0; i < els.length; i++) { + var t = (els[i].textContent || '').toLowerCase(); + var lbl = (els[i].getAttribute('aria-label') || '').toLowerCase(); + if (successKeywords.some(function(k) { return t.includes(k) || lbl.includes(k); })) { + found = true; + break; + } + } + } + window.ReactNativeWebView.postMessage(JSON.stringify({ status: found ? 'done_with_success' : 'done_without_success' })); + })(); + `; + if (webViewRef.current) { + webViewRef.current.injectJavaScript(checkScript); + } else { + navigation.goBack(); + } + }; + + const handleHttpError = (syntheticEvent: any) => { + const { nativeEvent } = syntheticEvent; + console.warn('WebView HTTP error: ', nativeEvent?.statusCode, nativeEvent?.description); + }; + + const handleError = (syntheticEvent: any) => { + const { nativeEvent } = syntheticEvent; + console.warn('WebView general loading error:', nativeEvent?.description); + if (!fallbackTriggered) { + setFallbackTriggered(true); + setShowFallbackOverlay(true); + } + }; + + // JS Injection: LinkedIn-specific Connect button highlighting & event detection + // injectedJavaScriptBeforeContentLoaded runs BEFORE any page content — sets up listeners early + const injectedJSBeforeLoad = platform === 'linkedin' ? ` + (function() { + // Set up the SUCCESS_KEYWORDS and postMessage bridge as early as possible + window.__devcardSuccessKeywords = [ + 'invite sent', 'invitation sent', 'request sent', + 'connection request sent', 'pending', 'withdraw' + ]; + window.__devcardSuccessReported = false; + window.__devcardHighlighted = false; + + window.__devcardCheck = function() { + if (window.__devcardSuccessReported) return; + var kws = window.__devcardSuccessKeywords; + var bodyText = document.body ? document.body.innerText.toLowerCase() : ''; + for (var k = 0; k < kws.length; k++) { + if (bodyText.includes(kws[k])) { + window.__devcardSuccessReported = true; + try { window.ReactNativeWebView.postMessage(JSON.stringify({ status: 'success' })); } catch(error){} + return; + } + } + var els = document.querySelectorAll('button, span, a, [role="button"]'); + for (var i = 0; i < els.length; i++) { + var t = (els[i].textContent || '').toLowerCase(); + var l = (els[i].getAttribute('aria-label') || '').toLowerCase(); + for (var j = 0; j < kws.length; j++) { + if (t.includes(kws[j]) || l.includes(kws[j])) { + window.__devcardSuccessReported = true; + try { window.ReactNativeWebView.postMessage(JSON.stringify({ status: 'success' })); } catch(error){} + return; + } + } + } + }; + + // Check when page becomes visible (fires after dialogs close) + document.addEventListener('visibilitychange', function() { + if (document.visibilityState === 'visible') { + setTimeout(window.__devcardCheck, 200); + setTimeout(window.__devcardCheck, 600); + } + }); + + // Check on focus events (modal dismissal, back navigation) + window.addEventListener('focus', function() { + setTimeout(window.__devcardCheck, 300); + }); + })(); + ` : undefined; + + const injectedJS = platform === 'linkedin' ? ` + (function() { + function log(msg) { + try { + window.ReactNativeWebView.postMessage(JSON.stringify({ status: 'debug', message: msg })); + } catch(error){} + } + + log('LinkedIn JS Engine Started'); + + // Inject pulsating highlight CSS for the Connect button + var styleEl = document.createElement('style'); + styleEl.innerHTML = [ + '@keyframes pulse-highlight {', + ' 0% { box-shadow: 0 0 0 0px rgba(10,102,194,0.7); border-color: #0A66C2; }', + ' 70% { box-shadow: 0 0 0 10px rgba(10,102,194,0); border-color: #0084FF; }', + ' 100% { box-shadow: 0 0 0 0px rgba(10,102,194,0); border-color: #0A66C2; }', + '}', + '.devcard-highlight {', + ' animation: pulse-highlight 2s infinite !important;', + ' border: 3px solid #0A66C2 !important;', + ' transform: scale(1.02) !important;', + '}' + ].join(''); + if (document.head) document.head.appendChild(styleEl); + + // Reuse globals set by injectedJavaScriptBeforeContentLoaded if available + var SUCCESS_KEYWORDS = (window.__devcardSuccessKeywords) || [ + 'invite sent', 'invitation sent', 'request sent', + 'connection request sent', 'pending', 'withdraw' + ]; + var successReported = (window.__devcardSuccessReported) || false; + var highlighted = (window.__devcardHighlighted) || false; + + function reportSuccess(reason) { + if (successReported) return; + successReported = true; + if (window.__devcardSuccessReported !== undefined) window.__devcardSuccessReported = true; + try { window.ReactNativeWebView.postMessage(JSON.stringify({ status: 'success' })); } catch(error){} + log('Success: ' + reason); + } + + function checkPage() { + if (successReported) return; + + // 1. Body text scan + var bodyText = document.body ? document.body.innerText.toLowerCase() : ''; + for (var k = 0; k < SUCCESS_KEYWORDS.length; k++) { + if (bodyText.includes(SUCCESS_KEYWORDS[k])) { + reportSuccess('body:' + SUCCESS_KEYWORDS[k]); + return; + } + } + + // 2. Element scan + var allEls = document.querySelectorAll('button, a, span, [role="button"], li'); + for (var i = 0; i < allEls.length; i++) { + var el = allEls[i]; + var text = (el.textContent || '').replace(new RegExp('\\s+', 'g'), ' ').trim().toLowerCase(); + var aria = (el.getAttribute('aria-label') || '').toLowerCase(); + var combined = text + ' ' + aria; + for (var j = 0; j < SUCCESS_KEYWORDS.length; j++) { + if (combined.includes(SUCCESS_KEYWORDS[j])) { + reportSuccess('element:' + combined.substring(0, 40)); + return; + } + } + // Highlight the Connect button + if (!highlighted) { + var isConnect = (text === 'connect' || aria === 'connect' || aria.includes('connect to')) + && !text.includes('connections') && !text.includes('connected') && !el.disabled; + if (isConnect) { + el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + el.classList.add('devcard-highlight'); + highlighted = true; + log('Connect button highlighted'); + } + } + } + } + + checkPage(); + + // MutationObserver — watches childList, subtree AND characterData + function startObserver() { + var obs = new MutationObserver(function(mutations) { checkPage(); }); + obs.observe(document.body, { + childList: true, subtree: true, characterData: true, attributes: true, + attributeFilter: ['aria-label', 'class', 'disabled'] + }); + log('MutationObserver active'); + } + + if (document.body) { + startObserver(); + } else { + document.addEventListener('DOMContentLoaded', startObserver); + } + + // Polling every 700ms (runs for up to 90 seconds) + var pollCount = 0; + var pollTimer = setInterval(function() { + pollCount++; + checkPage(); + if (successReported || pollCount > 128) clearInterval(pollTimer); + }, 700); + + // Also run check on popstate (LinkedIn SPA navigation) + window.addEventListener('popstate', function() { + setTimeout(checkPage, 300); + setTimeout(checkPage, 800); + }); + + log('Engine ready, polling + observer active'); + })(); + ` : undefined; + return ( - {/* Header Bar */} - - navigation.goBack()}> - ✕ Close - - {displayName} - + {/* Header Container */} + + + navigation.goBack()} activeOpacity={0.7}> + ✕ Close + + {platformDisplayName} + + + {/* Loading Progress Bar */} + {progress > 0 && progress < 1 && ( + + )} {/* Info Banner */} - Tap the Follow or{' '} - Connect button below to complete the action + You are viewing this profile in DevCard — tap Connect on {platformDisplayName} to send your request + {successToast && ( + + {successToast} + + )} + {/* WebView */} - ( - - Loading {displayName}... - - )} - onNavigationStateChange={(navState) => { - // If user navigates away from the profile page, - // they likely completed the action - // We could auto-close here in the future - }} - /> - - {/* Done Button */} + {url ? ( + + setProgress(nativeEvent.progress)} + onLoadEnd={() => setHasLoaded(true)} + onError={handleError} + onHttpError={handleHttpError} + onMessage={(event) => { + try { + const data = JSON.parse(event.nativeEvent.data); + if (data.status === 'success') { + handleSuccess(); + } else if (data.status === 'done_with_success') { + // Done button pressed: success found on current page + handleSuccess(); + } else if (data.status === 'done_without_success') { + // Done button pressed: no success found, just go back + navigation.goBack(); + } else if (data.status === 'debug') { + console.log('[WebView JS] ' + data.message); + } + } catch {} + }} + onNavigationStateChange={(navState) => { + // Detect final invite-sent/shared subroutes (exclude early pages like send-invite) + if ( + navState.url.includes('invite-sent') || + navState.url.includes('inviteShared') || + navState.url.includes('invitation-sent') + ) { + handleSuccess(); + } + }} + renderLoading={() => ( + + + + + + Loading {platformDisplayName}... + + )} + /> + + {/* Premium Fallback Overlay for slow load / timeouts */} + {showFallbackOverlay && ( + + + + Profile loading is slow + + {platformDisplayName} is taking longer than usual to load inside the app. Would you like to open it directly in the native app? + + + + Open in {platformDisplayName} App + + + + Open in Default Browser + + + + + Retry Loading + + navigation.goBack()} + activeOpacity={0.7}> + Cancel + + + + + )} + + ) : ( + + Invalid profile URL + + )} + + {/* Done Button Footer */} navigation.goBack()}> + onPress={handleDonePress} + activeOpacity={0.8}> Done @@ -93,25 +520,142 @@ const styles = StyleSheet.create({ header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: SPACING.md, borderBottomWidth: 1, borderBottomColor: COLORS.border, + backgroundColor: COLORS.bgSecondary, }, - closeText: { color: COLORS.textSecondary, fontSize: FONT_SIZE.md }, + closeText: { color: COLORS.textSecondary, fontSize: FONT_SIZE.md, fontWeight: '600' }, headerTitle: { fontSize: FONT_SIZE.md, fontWeight: '700', color: COLORS.textPrimary }, headerSpacer: { width: 60 }, + progressBar: { + height: 3, + position: 'absolute', + bottom: 0, + left: 0, + zIndex: 10, + }, banner: { backgroundColor: COLORS.bgCard, padding: SPACING.md, borderBottomWidth: 1, borderBottomColor: COLORS.border, }, - bannerText: { fontSize: FONT_SIZE.sm, color: COLORS.textSecondary, textAlign: 'center' }, + bannerText: { fontSize: FONT_SIZE.sm, color: COLORS.textSecondary, textAlign: 'center', lineHeight: 20 }, bannerBold: { fontWeight: '700', color: COLORS.primary }, + toast: { + position: 'absolute', + top: 118, + left: SPACING.md, + right: SPACING.md, + zIndex: 20, + backgroundColor: COLORS.success, + borderRadius: BORDER_RADIUS.md, + padding: SPACING.md, + alignItems: 'center', + ...SHADOWS.button, + }, + toastText: { color: COLORS.white, fontSize: FONT_SIZE.sm, fontWeight: '700' }, + webContainer: { flex: 1, position: 'relative' }, webview: { flex: 1 }, - loading: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: COLORS.bgPrimary }, - loadingText: { color: COLORS.textMuted, fontSize: FONT_SIZE.md }, + loading: { + ...StyleSheet.absoluteFillObject, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: COLORS.bgPrimary, + padding: SPACING.lg, + zIndex: 5, + }, + loadingBlock: { marginTop: SPACING.lg }, + loadingLine: { marginTop: SPACING.md }, + loadingText: { color: COLORS.textMuted, fontSize: FONT_SIZE.md, marginTop: SPACING.lg }, footer: { padding: SPACING.md, borderTopWidth: 1, borderTopColor: COLORS.border, + backgroundColor: COLORS.bgSecondary, }, doneButton: { - backgroundColor: COLORS.success, borderRadius: BORDER_RADIUS.md, + backgroundColor: COLORS.bgElevated, borderRadius: BORDER_RADIUS.md, padding: SPACING.md, alignItems: 'center', + borderWidth: 1, + borderColor: COLORS.border, + }, + doneButtonText: { color: COLORS.textPrimary, fontWeight: '700', fontSize: FONT_SIZE.md }, + + // Custom Fallback Overlay Styling + overlayContainer: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(15, 15, 26, 0.95)', + justifyContent: 'center', + alignItems: 'center', + padding: SPACING.lg, + zIndex: 50, + }, + overlayCard: { + backgroundColor: COLORS.bgSecondary, + borderRadius: BORDER_RADIUS.lg, + padding: SPACING.xl, + width: '100%', + maxWidth: 340, + alignItems: 'center', + borderWidth: 1, + borderColor: COLORS.border, + ...SHADOWS.card, + }, + overlayIcon: { + fontSize: 48, + marginBottom: SPACING.md, + }, + overlayTitle: { + fontSize: FONT_SIZE.lg, + fontWeight: '700', + color: COLORS.textPrimary, + marginBottom: SPACING.sm, + textAlign: 'center', + }, + overlayDescription: { + fontSize: FONT_SIZE.sm, + color: COLORS.textSecondary, + textAlign: 'center', + marginBottom: SPACING.lg, + lineHeight: 20, + }, + overlayPrimaryButton: { + backgroundColor: COLORS.primary, + borderRadius: BORDER_RADIUS.md, + paddingVertical: SPACING.md, + width: '100%', + alignItems: 'center', + marginBottom: SPACING.sm, + ...SHADOWS.button, + }, + overlayPrimaryButtonText: { + color: COLORS.white, + fontWeight: '700', + fontSize: FONT_SIZE.md, + }, + overlaySecondaryButton: { + backgroundColor: COLORS.bgElevated, + borderRadius: BORDER_RADIUS.md, + paddingVertical: SPACING.md, + width: '100%', + alignItems: 'center', + marginBottom: SPACING.lg, + borderWidth: 1, + borderColor: COLORS.border, + }, + overlaySecondaryButtonText: { + color: COLORS.textPrimary, + fontWeight: '600', + fontSize: FONT_SIZE.md, + }, + overlayRowButtons: { + flexDirection: 'row', + justifyContent: 'space-between', + width: '100%', + paddingHorizontal: SPACING.sm, + }, + overlayTextButton: { + paddingVertical: SPACING.sm, + paddingHorizontal: SPACING.md, + }, + overlayTextButtonText: { + color: COLORS.textMuted, + fontSize: FONT_SIZE.sm, + fontWeight: '600', }, - doneButtonText: { color: COLORS.white, fontWeight: '700', fontSize: FONT_SIZE.md }, }); diff --git a/apps/mobile/src/services/api.ts b/apps/mobile/src/services/api.ts new file mode 100644 index 00000000..70daf195 --- /dev/null +++ b/apps/mobile/src/services/api.ts @@ -0,0 +1,46 @@ +import { API_BASE_URL } from '../config'; + +type RequestOptions = { + method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; + body?: unknown; + token?: string | null; + onUnauthorized?: () => void; +}; + +export async function apiRequest( + path: string, + { method = 'GET', body, token, onUnauthorized }: RequestOptions = {} +): Promise { + const headers: Record = { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; + + const res = await fetch(`${API_BASE_URL}${path}`, { + method, + headers, + ...(body ? { body: JSON.stringify(body) } : {}), + }); + + if (res.status === 401 || res.status === 403) { + onUnauthorized?.(); + throw new Error('Unauthorized'); + } + + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error((err as any)?.message ?? `Request failed: ${res.status}`); + } + + // Some endpoints may return empty responses + const text = await res.text(); + if (!text) return (null as unknown) as T; + return JSON.parse(text) as T; +} + +export const get = (path: string, token?: string | null) => apiRequest(path, { method: 'GET', token }); +export const post = (path: string, body?: unknown, token?: string | null) => apiRequest(path, { method: 'POST', body, token }); +export const put = (path: string, body?: unknown, token?: string | null) => apiRequest(path, { method: 'PUT', body, token }); +export const del = (path: string, body?: unknown, token?: string | null) => apiRequest(path, { method: 'DELETE', body, token }); + +export default { apiRequest, get, post, put, del }; diff --git a/apps/mobile/src/types/index.ts b/apps/mobile/src/types/index.ts new file mode 100644 index 00000000..c815a5d0 --- /dev/null +++ b/apps/mobile/src/types/index.ts @@ -0,0 +1,100 @@ +// ── Centralized Mobile Type Definitions ─────────────────────────────────────── +// Re-exports shared types and defines mobile-only types to eliminate duplicate +// interface declarations across screens (was duplicated in 4+ files). + +export type { + User, + PlatformLink, + Card, + PublicProfile, + PublicCard, + FollowStatus, + FollowResult, + AuthResponse, + CardView, + AnalyticsOverview, + ConnectedPlatform, + FollowLog, + OAuthTokenInfo, + CreateLinkPayload, + UpdateProfilePayload, + CreateCardPayload, + UpdateCardPayload, + ReorderLinksPayload, +} from '@devcard/shared'; + +export type { PlatformDef, FollowStrategy } from '@devcard/shared'; + +// ── Mobile-only Types ───────────────────────────────────────────────────────── + +export interface SavedContact { + username: string; + displayName: string; + avatarUrl: string | null; + accentColor: string; + bio: string | null; + role: string | null; + company: string | null; + metAt: string | null; + note: string | null; + savedAt: string; +} + +export interface EventSummary { + id: string; + name: string; + slug: string; + location: string; + description: string | null; + startDate: string; + endDate: string; + attendeesCount: number; +} + +export interface EventDetail extends EventSummary { + organizerId: string; + createdAt: string; +} + +export interface EventAttendee { + id: string; + username: string; + displayName: string; + bio: string | null; + pronouns: string | null; + company: string | null; + avatarUrl: string | null; + accentColor: string; +} + +export interface TeamSummary { + id: string; + name: string; + slug: string; + description: string | null; + avatarUrl: string | null; + ownerId: string; + createdAt: string; + updatedAt: string | null; + members: TeamMember[]; +} + +export interface TeamMember { + username: string; + displayName: string; + bio: string | null; + pronouns: string | null; + role: string | null; + company: string | null; + avatarUrl: string | null; + accentColor: string; + teamRole: 'OWNER' | 'ADMIN' | 'MEMBER'; + joinedAt: string; +} + +export type FollowState = Record; + +export interface NfcPayload { + type: 'URI'; + payload: string; +} diff --git a/apps/web/src/app.css b/apps/web/src/app.css index bb09fef9..0f9e8bb0 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -3,48 +3,51 @@ :root { /* Primary Palette */ --primary: #6366f1; - --primary-glow: rgba(99, 102, 241, 0.5); + --primary-glow: rgba(99, 102, 241, 0.4); --accent: #a855f7; - --accent-glow: rgba(168, 85, 247, 0.4); - + --accent-glow: rgba(168, 85, 247, 0.35); + /* Backgrounds */ --bg-primary: #ffffff; --bg-secondary: #f8fafc; - --bg-glass: rgba(255, 255, 255, 0.7); + --bg-page: #eef2ff; + --bg-glass: rgba(255, 255, 255, 0.38); --bg-card: #ffffff; - + /* Text */ --text-primary: #0f172a; --text-secondary: #475569; - --text-muted: #94a3b8; - + --text-muted: #64748b; + /* Effects */ - --border: rgba(226, 232, 240, 0.8); - --border-glass: rgba(255, 255, 255, 0.3); + --border: rgba(226, 232, 240, 0.9); + --border-glass: rgba(99, 102, 241, 0.25); --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); - - --radius: 12px; - --radius-lg: 20px; - --radius-xl: 32px; - - --theme-transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + --shadow-md: 0 6px 18px -8px rgb(0 0 0 / 0.12), 0 4px 14px -12px rgb(0 0 0 / 0.08); + --shadow-lg: 0 12px 24px -10px rgb(0 0 0 / 0.15), 0 6px 12px -14px rgb(0 0 0 / 0.08); + --shadow-nav: 0 8px 32px -8px rgba(99, 102, 241, 0.18), 0 2px 8px 0 rgba(99, 102, 241, 0.08); + + --radius: 14px; + --radius-lg: 26px; + --radius-xl: 34px; + + --theme-transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); } html.dark { --bg-primary: #020617; --bg-secondary: #0f172a; - --bg-glass: rgba(15, 23, 42, 0.6); + --bg-page: #050b18; + --bg-glass: rgba(15, 23, 42, 0.72); --bg-card: #0f172a; - + --text-primary: #f8fafc; --text-secondary: #cbd5e1; --text-muted: #64748b; - - --border: rgba(30, 41, 59, 0.8); - --border-glass: rgba(255, 255, 255, 0.1); + + --border: rgba(30, 41, 59, 0.85); + --border-glass: rgba(255, 255, 255, 0.12); + --shadow-nav: 0 4px 24px -6px rgba(0, 0, 0, 0.45), 0 1px 4px 0 rgba(0, 0, 0, 0.25); } * { @@ -55,7 +58,11 @@ html.dark { body { font-family: 'Inter', sans-serif; - background-color: var(--bg-primary); + background: + radial-gradient(ellipse at 60% -10%, rgba(99, 102, 241, 0.28) 0%, transparent 55%), + radial-gradient(ellipse at -10% 80%, rgba(168, 85, 247, 0.18) 0%, transparent 45%), + radial-gradient(ellipse at 100% 60%, rgba(99, 102, 241, 0.12) 0%, transparent 40%), + var(--bg-page); color: var(--text-primary); transition: var(--theme-transition); -webkit-font-smoothing: antialiased; @@ -66,7 +73,7 @@ body { h1, h2, h3, h4, h5, h6 { font-family: 'Outfit', sans-serif; font-weight: 700; - line-height: 1.1; + line-height: 1.15; } a { @@ -75,11 +82,22 @@ a { transition: var(--theme-transition); } +button, +.btn-primary, +.btn-secondary { + transition: transform 0.24s ease, box-shadow 0.24s ease, background-color 0.24s ease, border-color 0.24s ease, color 0.24s ease; +} + +button { + font: inherit; +} + .glass { background: var(--bg-glass); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(18px); + -webkit-backdrop-filter: blur(18px); border: 1px solid var(--border-glass); + box-shadow: var(--shadow-nav); } .gradient-text { @@ -90,17 +108,99 @@ a { } .btn-primary { + display: inline-flex; + align-items: center; + justify-content: center; background: linear-gradient(135deg, var(--primary), var(--accent)); color: white; - padding: 0.8rem 1.6rem; - border-radius: var(--radius); - font-weight: 600; - box-shadow: 0 4px 15px var(--primary-glow); + padding: 0.95rem 1.85rem; + border-radius: calc(var(--radius) * 1.2); + font-weight: 700; + box-shadow: 0 18px 35px -18px rgba(99, 102, 241, 0.9); border: none; cursor: pointer; } .btn-primary:hover { transform: translateY(-2px); - box-shadow: 0 6px 20px var(--primary-glow); + box-shadow: 0 22px 40px -16px rgba(99, 102, 241, 0.9); +} + +.btn-primary:focus-visible { + outline: 3px solid rgba(99, 102, 241, 0.35); + outline-offset: 3px; +} + +.btn-secondary { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.85rem 1.75rem; + border-radius: calc(var(--radius) * 1.2); + font-weight: 700; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(255, 255, 255, 0.08); + color: var(--text-primary); + cursor: pointer; } + +.btn-secondary:hover { + background: rgba(255, 255, 255, 0.14); + border-color: rgba(99, 102, 241, 0.45); +} + +.btn-secondary:focus-visible { + outline: 3px solid rgba(99, 102, 241, 0.18); + outline-offset: 3px; +} + +/* ---------- Custom themed scrollbar (issue #151) ---------- */ + +/* Firefox */ +html { + scrollbar-width: thin; + scrollbar-color: var(--primary) var(--bg-secondary); +} + +/* WebKit (Chromium, Safari, Edge) */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: var(--bg-secondary); + border-radius: 999px; +} + +::-webkit-scrollbar-thumb { + background: linear-gradient(135deg, var(--primary), var(--accent)); + border-radius: 999px; + border: 2px solid var(--bg-secondary); + background-clip: padding-box; + transition: background 0.2s ease, box-shadow 0.2s ease; +} + +::-webkit-scrollbar-thumb:hover { + background: linear-gradient(135deg, var(--accent), var(--primary)); + box-shadow: 0 0 8px var(--primary-glow); +} + +::-webkit-scrollbar-corner { + background: var(--bg-secondary); +} + +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +/* Light mode btn-secondary fix */ +:root:not(.dark) .btn-secondary { + border-color: var(--border); + background: rgba(0, 0, 0, 0.04); +} \ No newline at end of file diff --git a/apps/web/src/app.html b/apps/web/src/app.html index f273cc58..666257e4 100644 --- a/apps/web/src/app.html +++ b/apps/web/src/app.html @@ -3,6 +3,11 @@ + + + + + %sveltekit.head% diff --git a/apps/web/src/lib/apiClient.ts b/apps/web/src/lib/apiClient.ts new file mode 100644 index 00000000..dbaad43f --- /dev/null +++ b/apps/web/src/lib/apiClient.ts @@ -0,0 +1,36 @@ +const API_BASE_URL = import.meta.env.PUBLIC_API_URL ?? 'http://localhost:3000'; + +type RequestOptions = { + method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; + body?: unknown; + token?: string | null; + onUnauthorized?: () => void; +}; + +export async function apiRequest( + endpoint: string, + { method = 'GET', body, token, onUnauthorized }: RequestOptions = {} +): Promise { + const headers: Record = { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; + + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + method, + headers, + ...(body ? { body: JSON.stringify(body) } : {}), + }); + + if (response.status === 401 || response.status === 403) { + onUnauthorized?.(); + throw new Error('Unauthorized'); + } + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error((error as any)?.message ?? `Request failed: ${response.status}`); + } + + return response.json() as Promise; +} \ No newline at end of file diff --git a/apps/web/src/routes/+page.svelte b/apps/web/src/routes/+page.svelte index 512f9053..efaa65e5 100644 --- a/apps/web/src/routes/+page.svelte +++ b/apps/web/src/routes/+page.svelte @@ -1,92 +1,3 @@ - - - - DevCard — One Tap. Every Profile. Every Platform. - - - -
- -
- - -
-
GSSoC'26 Edition
-

One Tap. Every Profile.
Every Platform.

-

- The open-source standard for developer networking. Put all your profiles—GitHub, LinkedIn, LeetCode, and more—into a single, high-impact digital card. -

- -
- -
-
-
🔗
-

Unified Identity

-

Combine your fragmented online presence into a cohesive professional identity.

-
-
-
-

Instant Follow

-

Integrated APIs allow followers to connect with you instantly across platforms.

-
-
-
🔒
-

Private by Design

-

No tracking, no data selling. Your information stays where it belongs: with you.

-
-
- -
-

© 2026 DevCard • Built for the Developer Community

-
-
- + \ No newline at end of file diff --git a/apps/web/src/routes/devcard/[id]/+page.server.ts b/apps/web/src/routes/devcard/[id]/+page.server.ts index adc98179..a93fbc75 100644 --- a/apps/web/src/routes/devcard/[id]/+page.server.ts +++ b/apps/web/src/routes/devcard/[id]/+page.server.ts @@ -1,17 +1,28 @@ import { error } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; +const API_BASE = process.env.BACKEND_URL || 'http://localhost:3000'; + export const load: PageServerLoad = async ({ params, fetch }) => { const { id } = params; - // Use internal fetch to reach the backend - // In production, this would be the actual API URL - const res = await fetch(`http://localhost:3000/api/u/card/${id}`); + try { + const res = await fetch(`${API_BASE}/api/u/card/${id}`); - if (!res.ok) { - throw error(404, 'Card not found'); - } + if (res.status === 404) { + throw error(404, 'Card not found'); + } - const card = await res.json(); - return { card }; + if (!res.ok) { + throw error(500, 'Failed to load card'); + } + + const card = await res.json(); + return { card }; + } catch (error) { + if (error && typeof error === 'object' && 'status' in error) { + throw error; + } + throw error(500, 'Failed to connect to backend'); + } }; diff --git a/apps/web/src/routes/devcard/[id]/+page.svelte b/apps/web/src/routes/devcard/[id]/+page.svelte index a38073fe..7423f7ba 100644 --- a/apps/web/src/routes/devcard/[id]/+page.svelte +++ b/apps/web/src/routes/devcard/[id]/+page.svelte @@ -104,7 +104,7 @@ diff --git a/apps/web/src/routes/u/[username]/+page.svelte b/apps/web/src/routes/u/[username]/+page.svelte index d75e485c..50cb4226 100644 --- a/apps/web/src/routes/u/[username]/+page.svelte +++ b/apps/web/src/routes/u/[username]/+page.svelte @@ -15,9 +15,48 @@ }; let mounted = $state(false); + let copyMessage = $state(''); + let copyStatus = $state<'success' | 'error'>('success'); + let copyMessageTimeout: ReturnType; + onMount(() => { mounted = true; + + return () => { + if (copyMessageTimeout) { + clearTimeout(copyMessageTimeout); + } + }; }); + + function showCopyMessage(message: string, status: 'success' | 'error') { + copyMessage = message; + copyStatus = status; + + if (copyMessageTimeout) { + clearTimeout(copyMessageTimeout); + } + + clearTimeout(copyTimeout); + + copyTimeout = setTimeout(() => { + copyMessage = ''; + }, 3000); + } + + async function copyProfileUrl() { + if (!navigator.clipboard?.writeText) { + showCopyMessage('Clipboard API unavailable. Copy the URL from your address bar.', 'error'); + return; + } + + try { + await navigator.clipboard.writeText(window.location.href); + showCopyMessage('Profile link copied.', 'success'); + } catch { + showCopyMessage('Could not copy link. Copy the URL from your address bar.', 'error'); + } + } @@ -96,7 +135,17 @@

Want a card like this?

- Create your DevCard ⚡ +
+ Create your DevCard ⚡ + +
+ {#if copyMessage} +

+ {copyMessage} +

+ {/if}
{/if} @@ -110,7 +159,7 @@ bottom: 0; background: radial-gradient(circle at 50% 0%, var(--accent), transparent 50%), #020617; - opacity: 0.15; + opacity: 0.18; z-index: -1; } @@ -119,10 +168,10 @@ display: flex; flex-direction: column; align-items: center; - padding: 4rem 1.5rem; + padding: clamp(2rem, 6vw, 5rem) 1.25rem 3rem; opacity: 0; - transform: translateY(20px); - transition: all 0.8s cubic-bezier(0.2, 0.8, 0.2, 1); + transform: translateY(22px); + transition: opacity 0.65s ease, transform 0.65s ease; } .profile-container.loaded { @@ -132,65 +181,67 @@ .profile-card { width: 100%; - max-width: 480px; + max-width: 540px; border-radius: var(--radius-xl); - padding: 3rem 2rem; - box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); + padding: 2.5rem 2rem; + box-shadow: 0 26px 60px -20px rgba(0, 0, 0, 0.55); position: relative; overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(15, 23, 42, 0.96); } .profile-header { text-align: center; - margin-bottom: 3rem; + margin-bottom: 2.5rem; } .avatar-wrapper { position: relative; - width: 110px; - height: 110px; - margin: 0 auto 1.5rem; + width: 120px; + height: 120px; + margin: 0 auto 1.75rem; } .avatar { width: 100%; height: 100%; - border-radius: 35% 65% 70% 30% / 30% 30% 70% 70%; + border-radius: 32% 68% 63% 37% / 34% 36% 64% 66%; object-fit: cover; - border: 3px solid white; + border: 3px solid rgba(255, 255, 255, 0.18); position: relative; z-index: 2; - animation: morph 8s ease-in-out infinite; - } - - @keyframes morph { - 0%, 100% { border-radius: 35% 65% 70% 30% / 30% 30% 70% 70%; } - 50% { border-radius: 65% 35% 30% 70% / 70% 70% 30% 30%; } } - .avatar-glow { - position: absolute; - top: 0; left: 0; right: 0; bottom: 0; - filter: blur(20px); - opacity: 0.4; - z-index: 1; - border-radius: 50%; + .avatar-placeholder { + width: 100%; + height: 100%; + border-radius: 32% 68% 63% 37% / 34% 36% 64% 66%; + display: flex; + align-items: center; + justify-content: center; + font-size: 3rem; + font-weight: 800; + color: white; } .display-name { - font-size: 2.25rem; + font-size: clamp(2rem, 4vw, 2.5rem); font-weight: 800; - letter-spacing: -1px; - margin-bottom: 0.5rem; + letter-spacing: -0.5px; + margin-bottom: 0.75rem; } .role-badge { - display: inline-block; - padding: 0.4rem 1rem; - background: rgba(255, 255, 255, 0.1); - border-radius: 100px; - font-size: 0.85rem; - font-weight: 600; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.45rem 1rem; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 999px; + font-size: 0.9rem; + font-weight: 700; color: var(--text-secondary); margin-bottom: 1rem; } @@ -198,72 +249,83 @@ .bio { color: var(--text-secondary); font-size: 1rem; - line-height: 1.6; - max-width: 320px; + line-height: 1.85; + max-width: 640px; margin: 0 auto; } .links-grid { display: flex; flex-direction: column; - gap: 0.75rem; + gap: 1rem; } .link-tile { display: flex; align-items: center; padding: 1rem; - border-radius: var(--radius-lg); - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border-radius: calc(var(--radius) * 1.1); + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.06); + box-shadow: 0 12px 30px -18px rgba(0, 0, 0, 0.35); + transition: transform 0.25s ease, background 0.25s ease, border-color 0.25s ease; animation: slideIn 0.5s ease-out forwards; animation-delay: var(--delay); opacity: 0; } + .link-tile:hover, + .link-tile:focus-visible { + background: rgba(255, 255, 255, 0.13); + transform: translateY(-2px); + border-color: rgba(99, 102, 241, 0.35); + } + + .link-tile:focus-visible { + outline: 3px solid rgba(99, 102, 241, 0.2); + outline-offset: 3px; + } + @keyframes slideIn { from { opacity: 0; transform: translateX(-20px); } to { opacity: 1; transform: translateX(0); } } - .link-tile:hover { - background: rgba(255, 255, 255, 0.15); - transform: scale(1.02) translateX(5px); - } - .tile-icon { - width: 44px; - height: 44px; - border-radius: 12px; + width: 46px; + height: 46px; + border-radius: 15px; display: flex; align-items: center; justify-content: center; color: white; font-weight: 800; - font-size: 1.2rem; - box-shadow: 0 4px 12px rgba(0,0,0,0.2); + font-size: 1.1rem; + box-shadow: 0 8px 18px -10px rgba(0,0,0,0.4); } .tile-content { flex: 1; - margin-left: 1.25rem; + margin-left: 1.1rem; } .platform-name { display: block; font-weight: 700; - font-size: 1.05rem; + font-size: 1rem; } .username { display: block; - font-size: 0.85rem; + font-size: 0.9rem; color: var(--text-muted); + margin-top: 0.1rem; } .arrow { - opacity: 0.3; + opacity: 0.45; font-size: 1.2rem; - transition: all 0.3s; + transition: transform 0.25s ease, opacity 0.25s ease; } .link-tile:hover .arrow { @@ -272,48 +334,104 @@ } .card-footer { - margin-top: 3rem; - padding-top: 2rem; - border-top: 1px solid rgba(255,255,255,0.05); + margin-top: 2.5rem; + padding-top: 1.75rem; + border-top: 1px solid rgba(255,255,255,0.08); display: flex; justify-content: space-between; align-items: center; color: var(--text-muted); - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 1px; + font-size: 0.82rem; + gap: 1rem; + flex-wrap: wrap; } .logo-sm { color: var(--text-secondary); font-family: 'Outfit', sans-serif; + font-weight: 700; } .get-your-own { - margin-top: 3rem; + margin-top: 2rem; text-align: center; } .get-your-own p { - font-size: 0.9rem; - color: var(--text-muted); margin-bottom: 0.5rem; + font-size: 0.95rem; + color: var(--text-muted); } - .get-your-own a { + .profile-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 0.75rem; + } + + .get-devcard-link { font-weight: 700; - font-size: 1.1rem; + font-size: 1.05rem; + } + + .copy-link-button { + border: 1px solid var(--border-glass); + border-radius: var(--radius); + background: rgba(255, 255, 255, 0.08); + color: var(--text-primary); + cursor: pointer; + font: inherit; + font-weight: 700; + padding: 0.65rem 1rem; + transition: all 0.2s ease; + } + + .copy-link-button:hover { + background: rgba(255, 255, 255, 0.15); + transform: translateY(-1px); + } + + .copy-link-button:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; + } + + .copy-message { + min-height: 1.2rem; + margin-top: 0.75rem; + margin-bottom: 0; + font-size: 0.85rem; + } + + .copy-message.success { + color: var(--text-secondary); + } + + .copy-message.error { + color: #ef4444; } .error-glass { text-align: center; - padding: 4rem; + padding: 3rem; border-radius: var(--radius-xl); + width: min(100%, 520px); } - @media (max-width: 480px) { + @media (max-width: 720px) { .profile-card { padding: 2rem 1.5rem; } - .display-name { font-size: 1.75rem; } + .profile-header { margin-bottom: 2rem; } + .avatar-wrapper { width: 108px; height: 108px; margin-bottom: 1.5rem; } + .card-footer { flex-direction: column; align-items: flex-start; } + } + + @media (max-width: 520px) { + .profile-container { padding: 2rem 1rem 2.5rem; } + .display-name { font-size: 2rem; } + .link-tile { padding: 0.95rem; } + .tile-content { margin-left: 0.9rem; } + .card-footer { text-align: left; } } diff --git a/docker-compose.yml b/docker-compose.yml index 0786787a..cfa524ca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: container_name: devcard-postgres restart: unless-stopped ports: - - '5432:5432' + - '5433:5432' environment: POSTGRES_USER: devcard POSTGRES_PASSWORD: devcard diff --git a/packages/shared/src/__tests__/cards.test.ts b/packages/shared/src/__tests__/cards.test.ts new file mode 100644 index 00000000..0c1a6d1e --- /dev/null +++ b/packages/shared/src/__tests__/cards.test.ts @@ -0,0 +1,72 @@ +import { describe, it, expect } from 'vitest'; +import { validateCardPlatforms, diffCardPlatforms } from '../cards'; + +describe('validateCardPlatforms', () => { + it('passes with valid platforms', () => { + const result = validateCardPlatforms(['github', 'linkedin']); + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it('fails with empty array', () => { + const result = validateCardPlatforms([]); + expect(result.valid).toBe(false); + expect(result.errors).toContain('At least one platform is required.'); + }); + + it('fails with unknown platform', () => { + const result = validateCardPlatforms(['github', 'myspace']); + expect(result.valid).toBe(false); + expect(result.errors.some(e => e.includes('myspace'))).toBe(true); + }); + + it('fails with duplicate platforms', () => { + const result = validateCardPlatforms(['github', 'github']); + expect(result.valid).toBe(false); + expect(result.errors.some(e => e.includes('Duplicate'))).toBe(true); + }); + + it('passes with exactly 10 platforms', () => { + const platforms = ['github','linkedin','twitter','youtube','twitch', + 'discord','devto','medium','dribbble','leetcode']; + const result = validateCardPlatforms(platforms); + expect(result.valid).toBe(true); + }); + + it('fails with more than 10 platforms', () => { + const platforms = ['github','linkedin','twitter','youtube','twitch', + 'discord','devto','medium','dribbble','leetcode','npm']; + const result = validateCardPlatforms(platforms); + expect(result.valid).toBe(false); + expect(result.errors.some(e => e.includes('Maximum 10'))).toBe(true); + }); + + it('fails with all invalid platforms', () => { + const result = validateCardPlatforms(['myspace', 'bebo']); + expect(result.valid).toBe(false); + expect(result.errors.length).toBeGreaterThanOrEqual(2); + }); +}); + +describe('diffCardPlatforms', () => { + it('correctly identifies added, removed, unchanged', () => { + const diff = diffCardPlatforms(['github', 'linkedin'], ['github', 'twitter']); + expect(diff.added).toEqual(['twitter']); + expect(diff.removed).toEqual(['linkedin']); + expect(diff.unchanged).toEqual(['github']); + }); + + it('handles empty old card', () => { + const diff = diffCardPlatforms([], ['github']); + expect(diff.added).toEqual(['github']); + expect(diff.removed).toEqual([]); + expect(diff.unchanged).toEqual([]); + }); + + it('handles identical cards', () => { + const diff = diffCardPlatforms(['github'], ['github']); + expect(diff.added).toEqual([]); + expect(diff.removed).toEqual([]); + expect(diff.unchanged).toEqual(['github']); + }); +}); \ No newline at end of file diff --git a/packages/shared/src/__tests__/platforms-url.test.ts b/packages/shared/src/__tests__/platforms-url.test.ts new file mode 100644 index 00000000..cbfac373 --- /dev/null +++ b/packages/shared/src/__tests__/platforms-url.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from 'vitest'; +import { getProfileUrl, getWebViewUrl, getDeepLinkUrl } from '../platforms'; + +// ─── getProfileUrl Tests ─── + +describe('getProfileUrl', () => { + it('should return the correct GitHub profile URL', () => { + expect(getProfileUrl('github', 'octocat')).toBe('https://github.com/octocat'); + }); + + it('should return the correct LinkedIn profile URL', () => { + expect(getProfileUrl('linkedin', 'john')).toBe('https://www.linkedin.com/in/john'); + }); + + it('should return the correct Twitter profile URL', () => { + expect(getProfileUrl('twitter', 'john')).toBe('https://x.com/john'); + }); + + it('should return empty string for an unknown platform', () => { + expect(getProfileUrl('nonexistent', 'user')).toBe(''); + }); +}); + +// ─── getWebViewUrl Tests ─── + +describe('getWebViewUrl', () => { + it('should return the correct LinkedIn webview URL', () => { + expect(getWebViewUrl('linkedin', 'john')).toBe('https://www.linkedin.com/in/john'); + }); + + it('should return the correct Twitter webview URL', () => { + expect(getWebViewUrl('twitter', 'john')).toBe('https://x.com/john'); + }); + + it('should return null for platforms without a webview URL (github)', () => { + expect(getWebViewUrl('github', 'octocat')).toBeNull(); + }); + + it('should return null for an unknown platform', () => { + expect(getWebViewUrl('nonexistent', 'user')).toBeNull(); + }); +}); + +// ─── getDeepLinkUrl Tests ─── + +describe('getDeepLinkUrl', () => { + it('should return the correct Twitter deep link URL', () => { + expect(getDeepLinkUrl('twitter', 'john')).toBe('twitter://user?screen_name=john'); + }); + + it('should return the correct LinkedIn deep link URL', () => { + expect(getDeepLinkUrl('linkedin', 'john')).toBe('linkedin://profile?id=john'); + }); + + it('should return null for platforms without a deep link (github)', () => { + expect(getDeepLinkUrl('github', 'octocat')).toBeNull(); + }); + + it('should return null for an unknown platform', () => { + expect(getDeepLinkUrl('nonexistent', 'user')).toBeNull(); + }); +}); diff --git a/packages/shared/src/cards.ts b/packages/shared/src/cards.ts new file mode 100644 index 00000000..d9fa5130 --- /dev/null +++ b/packages/shared/src/cards.ts @@ -0,0 +1,50 @@ +export type CardValidationResult = { + valid: boolean; + errors: string[]; +}; + +const PLATFORMS = new Set([ + 'github', 'linkedin', 'twitter', 'instagram', 'youtube', + 'twitch', 'discord', 'devto', 'hashnode', 'medium', + 'dribbble', 'behance', 'figma', 'stackoverflow', 'leetcode', + 'codepen', 'replit', 'npm', 'producthunt', 'website', +]); + +export function validateCardPlatforms(platforms: string[]): CardValidationResult { + const errors: string[] = []; + + if (platforms.length === 0) { + errors.push('At least one platform is required.'); + } + + if (platforms.length > 10) { + errors.push(`Maximum 10 platforms allowed, got ${platforms.length}.`); + } + + const seen = new Set(); + for (const p of platforms) { + if (!PLATFORMS.has(p)) { + errors.push(`Unknown platform: "${p}".`); + } + if (seen.has(p)) { + errors.push(`Duplicate platform: "${p}".`); + } + seen.add(p); + } + + return { valid: errors.length === 0, errors }; +} + +export function diffCardPlatforms( + oldCard: string[], + newCard: string[] +): { added: string[]; removed: string[]; unchanged: string[] } { + const oldSet = new Set(oldCard); + const newSet = new Set(newCard); + + return { + added: newCard.filter(p => !oldSet.has(p)), + removed: oldCard.filter(p => !newSet.has(p)), + unchanged: oldCard.filter(p => newSet.has(p)), + }; +} \ No newline at end of file diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index a57e7e77..409d3e76 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,2 +1,3 @@ export * from './platforms'; export * from './types'; +export * from './cards'; \ No newline at end of file diff --git a/packages/shared/src/platforms.test.ts b/packages/shared/src/platforms.test.ts index d1ff86b7..6ce07a0b 100644 --- a/packages/shared/src/platforms.test.ts +++ b/packages/shared/src/platforms.test.ts @@ -74,3 +74,38 @@ describe('getWebViewUrl / getDeepLinkUrl – stackoverflow', () => { expect(getDeepLinkUrl('stackoverflow', '1234/user')).toBeNull(); }); }); + +// ─── validationRegex Tests ─── + +describe('validationRegex logic', () => { + it('should correctly validate github usernames', () => { + const regex = PLATFORMS.github.validationRegex!; + expect(regex.test('valid-user')).toBe(true); + expect(regex.test('a')).toBe(true); + expect(regex.test('user123')).toBe(true); + // Invalid + expect(regex.test('-invalid')).toBe(false); + expect(regex.test('invalid-')).toBe(false); + expect(regex.test('in--valid')).toBe(false); + expect(regex.test('user name')).toBe(false); + }); + + it('should correctly validate linkedin usernames', () => { + const regex = PLATFORMS.linkedin.validationRegex!; + expect(regex.test('valid-user')).toBe(true); + expect(regex.test('user123')).toBe(true); + // Invalid + expect(regex.test('ab')).toBe(false); // Too short + expect(regex.test('user name')).toBe(false); + }); + + it('should correctly validate twitter usernames', () => { + const regex = PLATFORMS.twitter.validationRegex!; + expect(regex.test('valid_user')).toBe(true); + expect(regex.test('user123')).toBe(true); + // Invalid + expect(regex.test('user-name')).toBe(false); // Hyphens not allowed + expect(regex.test('this_is_a_very_long_name_indeed')).toBe(false); // Too long + expect(regex.test('user name')).toBe(false); + }); +}); diff --git a/packages/shared/src/platforms.ts b/packages/shared/src/platforms.ts index a218957c..81c81ab4 100644 --- a/packages/shared/src/platforms.ts +++ b/packages/shared/src/platforms.ts @@ -27,6 +27,8 @@ export interface PlatformDef { usernamePlaceholder: string; /** Whether the platform uses full URL instead of username */ usesFullUrl: boolean; + /** Regex pattern to validate usernames */ + validationRegex?: RegExp; } // ─── Platform Registry ─── @@ -44,6 +46,7 @@ export const PLATFORMS: Record = { oauthScopes: ['user:follow', 'read:user'], usernamePlaceholder: 'e.g. octocat', usesFullUrl: false, + validationRegex: /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/, }, linkedin: { id: 'linkedin', @@ -57,6 +60,7 @@ export const PLATFORMS: Record = { oauthScopes: ['r_liteprofile'], usernamePlaceholder: 'e.g. johndoe', usesFullUrl: false, + validationRegex: /^[a-zA-Z0-9-]{3,100}$/, }, twitter: { id: 'twitter', @@ -70,6 +74,7 @@ export const PLATFORMS: Record = { oauthScopes: [], usernamePlaceholder: 'e.g. elonmusk', usesFullUrl: false, + validationRegex: /^[A-Za-z0-9_]{1,15}$/, }, gitlab: { id: 'gitlab', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68186049..b08a8f46 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,24 +32,27 @@ importers: '@fastify/multipart': specifier: ^9.0.0 version: 9.4.0 + '@fastify/rate-limit': + specifier: ^10.3.0 + version: 10.3.0 '@fastify/static': specifier: ^8.0.0 version: 8.3.0 '@prisma/client': specifier: ^6.0.0 - version: 6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3) + version: 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3) dotenv: specifier: ^16.4.0 version: 16.6.1 fastify: specifier: ^5.0.0 - version: 5.8.2 + version: 5.8.5 fastify-plugin: specifier: ^5.0.0 version: 5.1.0 ioredis: specifier: ^5.4.0 - version: 5.10.0 + version: 5.11.0 qrcode: specifier: ^1.5.0 version: 1.5.4 @@ -59,25 +62,49 @@ importers: devDependencies: '@types/node': specifier: ^22.0.0 - version: 22.19.15 + version: 22.19.19 '@types/qrcode': specifier: ^1.5.0 version: 1.5.6 + eslint: + specifier: ^10.4.0 + version: 10.4.1(jiti@2.7.0) + eslint-import-resolver-typescript: + specifier: ^4.4.4 + version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0)))(eslint@10.4.1(jiti@2.7.0)) + eslint-plugin-import-x: + specifier: ^4.16.2 + version: 4.16.2(@typescript-eslint/utils@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0)) + eslint-plugin-n: + specifier: ^18.0.1 + version: 18.0.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-promise: + specifier: ^7.3.0 + version: 7.3.0(eslint@10.4.1(jiti@2.7.0)) + eslint-plugin-security: + specifier: ^4.0.0 + version: 4.0.0 + eslint-plugin-unicorn: + specifier: ^64.0.0 + version: 64.0.0(eslint@10.4.1(jiti@2.7.0)) pino-pretty: specifier: ^13.1.3 version: 13.1.3 prisma: specifier: ^6.0.0 - version: 6.19.2(typescript@5.9.3) + version: 6.19.3(typescript@5.9.3) tsx: specifier: ^4.0.0 - version: 4.21.0 + version: 4.22.3 typescript: specifier: ^5.4.0 version: 5.9.3 + typescript-eslint: + specifier: ^8.59.3 + version: 8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@22.19.15)(terser@5.46.0) + version: 2.1.9(@types/node@22.19.19)(terser@5.48.0) apps/mobile: dependencies: @@ -86,68 +113,77 @@ importers: version: link:../../packages/shared '@gorhom/bottom-sheet': specifier: ^5.0.5 - version: 5.2.14(@types/react-native@0.70.19)(@types/react@19.2.14)(react-native-gesture-handler@2.31.2(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-reanimated@3.19.5(@babel/core@7.29.0)(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + version: 5.2.14(@types/react-native@0.70.19)(@types/react@19.2.15)(react-native-gesture-handler@2.31.2(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native-reanimated@3.19.5(@babel/core@7.29.7)(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) '@react-native-async-storage/async-storage': specifier: ^2.1.0 - version: 2.2.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3)) + version: 2.2.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3)) '@react-native/new-app-screen': specifier: 0.84.1 - version: 0.84.1(@types/react@19.2.14)(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + version: 0.84.1(@types/react@19.2.15)(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) '@react-navigation/bottom-tabs': specifier: ^7.0.0 - version: 7.15.5(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + version: 7.16.2(@react-navigation/native@7.2.5(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.8.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native-screens@4.25.2(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) '@react-navigation/native': specifier: ^7.0.0 - version: 7.1.33(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + version: 7.2.5(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) '@react-navigation/native-stack': specifier: ^7.0.0 - version: 7.14.4(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + version: 7.16.0(@react-navigation/native@7.2.5(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.8.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native-screens@4.25.2(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) react: specifier: 19.2.3 version: 19.2.3 react-dom: - specifier: ^19.2.4 - version: 19.2.4(react@19.2.3) + specifier: ^19.2.3 + version: 19.2.6(react@19.2.3) react-native: specifier: 0.84.1 - version: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) + version: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) + react-native-camera-kit: + specifier: ^14.0.0 + version: 14.2.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) react-native-gesture-handler: - specifier: ^2.20.2 - version: 2.31.2(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + specifier: ^2.28.0 + version: 2.31.2(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) react-native-qrcode-svg: specifier: ^6.3.0 - version: 6.3.21(react-native-svg@15.15.3(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + version: 6.3.21(react-native-svg@15.15.5(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) react-native-reanimated: - specifier: ^3.15.0 - version: 3.19.5(@babel/core@7.29.0)(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + specifier: ^3.16.7 + version: 3.19.5(@babel/core@7.29.7)(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) react-native-safe-area-context: specifier: ^5.5.2 - version: 5.7.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + version: 5.8.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) react-native-screens: specifier: ^4.0.0 - version: 4.24.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + version: 4.25.2(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) react-native-svg: specifier: ^15.0.0 - version: 15.15.3(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + version: 15.15.5(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) react-native-vector-icons: specifier: ^10.0.0 version: 10.3.0 + react-native-view-shot: + specifier: ^5.1.0 + version: 5.1.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) react-native-web: specifier: ^0.21.2 - version: 0.21.2(react-dom@19.2.4(react@19.2.3))(react@19.2.3) + version: 0.21.2(react-dom@19.2.6(react@19.2.3))(react@19.2.3) react-native-webview: specifier: ^13.0.0 - version: 13.16.1(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + version: 13.16.1(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) + react-native-worklets: + specifier: 0.5.1 + version: 0.5.1(@babel/core@7.29.7)(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) devDependencies: '@babel/core': specifier: ^7.25.2 - version: 7.29.0 + version: 7.29.7 '@babel/preset-env': specifier: ^7.25.3 - version: 7.29.0(@babel/core@7.29.0) + version: 7.29.7(@babel/core@7.29.7) '@babel/runtime': specifier: ^7.25.0 - version: 7.28.6 + version: 7.29.7 '@react-native-community/cli': specifier: 20.1.0 version: 20.1.0(typescript@5.9.3) @@ -159,19 +195,19 @@ importers: version: 20.1.0 '@react-native/babel-preset': specifier: 0.84.1 - version: 0.84.1(@babel/core@7.29.0) + version: 0.84.1(@babel/core@7.29.7) '@react-native/codegen': specifier: 0.84.1 - version: 0.84.1(@babel/core@7.29.0) + version: 0.84.1(@babel/core@7.29.7) '@react-native/eslint-config': specifier: 0.84.1 - version: 0.84.1(eslint@8.57.1)(jest@29.7.0(@types/node@22.19.15))(prettier@2.8.8)(typescript@5.9.3) + version: 0.84.1(eslint@8.57.1)(jest@29.7.0(@types/node@22.19.19))(prettier@2.8.8)(typescript@5.9.3) '@react-native/gradle-plugin': specifier: 0.84.1 version: 0.84.1 '@react-native/metro-config': specifier: 0.84.1 - version: 0.84.1(@babel/core@7.29.0) + version: 0.84.1(@babel/core@7.29.7) '@react-native/typescript-config': specifier: 0.84.1 version: 0.84.1 @@ -180,7 +216,7 @@ importers: version: 29.5.14 '@types/react': specifier: ^19.2.0 - version: 19.2.14 + version: 19.2.15 '@types/react-native-vector-icons': specifier: ^6.4.18 version: 6.4.18 @@ -192,7 +228,7 @@ importers: version: 8.57.1 jest: specifier: ^29.6.3 - version: 29.7.0(@types/node@22.19.15) + version: 29.7.0(@types/node@22.19.19) prettier: specifier: 2.8.8 version: 2.8.8 @@ -211,25 +247,25 @@ importers: devDependencies: '@sveltejs/adapter-auto': specifier: ^7.0.0 - version: 7.0.1(@sveltejs/kit@2.54.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.10)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.10)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))) + version: 7.0.1(@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.56.0(@typescript-eslint/types@8.60.0))(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.56.0(@typescript-eslint/types@8.60.0))(typescript@5.9.3)(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0))) '@sveltejs/kit': specifier: ^2.50.2 - version: 2.54.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.10)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.10)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 2.61.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.56.0(@typescript-eslint/types@8.60.0))(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.56.0(@typescript-eslint/types@8.60.0))(typescript@5.9.3)(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)) '@sveltejs/vite-plugin-svelte': specifier: ^6.2.4 - version: 6.2.4(svelte@5.53.10)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.2.4(svelte@5.56.0(@typescript-eslint/types@8.60.0))(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)) svelte: specifier: ^5.51.0 - version: 5.53.10 + version: 5.56.0(@typescript-eslint/types@8.60.0) svelte-check: specifier: ^4.4.2 - version: 4.4.5(picomatch@4.0.3)(svelte@5.53.10)(typescript@5.9.3) + version: 4.4.8(picomatch@4.0.4)(svelte@5.56.0(@typescript-eslint/types@8.60.0))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 vite: specifier: ^7.3.1 - version: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0) packages/shared: devDependencies: @@ -238,157 +274,163 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@22.19.15)(terser@5.46.0) + version: 2.1.9(@types/node@22.19.19)(terser@5.48.0) packages: - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.29.0': - resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} engines: {node: '>=6.9.0'} - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/eslint-parser@7.28.6': - resolution: {integrity: sha512-QGmsKi2PBO/MHSQk+AAgA9R6OHQr+VqnniFE0eMWZcVcfBZoA2dKn2hUsl3Csg/Plt9opRUWdY7//VXsrIlEiA==} + '@babel/eslint-parser@7.29.7': + resolution: {integrity: sha512-zxt+UJTOMKvUt3yOg+D58MLuz334pHp93qifMFcjIIO+9hN6t+ufw2gi7vDPMpxvfnHRR+3VVXvIjineCcgyXw==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} peerDependencies: '@babel/core': ^7.11.0 eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} engines: {node: '>=6.9.0'} - '@babel/helper-annotate-as-pure@7.27.3': - resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + '@babel/helper-annotate-as-pure@7.29.7': + resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.28.6': - resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} + '@babel/helper-create-class-features-plugin@7.29.7': + resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-create-regexp-features-plugin@7.28.5': - resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==} + '@babel/helper-create-regexp-features-plugin@7.29.7': + resolution: {integrity: sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-define-polyfill-provider@0.6.7': - resolution: {integrity: sha512-6Fqi8MtQ/PweQ9xvux65emkLQ83uB+qAVtfHkC9UodyHMIZdxNI01HjLCLUtybElp2KY2XNE0nOgyP1E1vXw9w==} + '@babel/helper-define-polyfill-provider@0.6.8': + resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} - '@babel/helper-member-expression-to-functions@7.28.5': - resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + '@babel/helper-member-expression-to-functions@7.29.7': + resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-optimise-call-expression@7.27.1': - resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + '@babel/helper-optimise-call-expression@7.29.7': + resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==} engines: {node: '>=6.9.0'} - '@babel/helper-plugin-utils@7.28.6': - resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} engines: {node: '>=6.9.0'} - '@babel/helper-remap-async-to-generator@7.27.1': - resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} + '@babel/helper-remap-async-to-generator@7.29.7': + resolution: {integrity: sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-replace-supers@7.28.6': - resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + '@babel/helper-replace-supers@7.29.7': + resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-skip-transparent-expression-wrappers@7.27.1': - resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} engines: {node: '>=6.9.0'} - '@babel/helper-wrap-function@7.28.6': - resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==} + '@babel/helper-wrap-function@7.29.7': + resolution: {integrity: sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.28.6': - resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.0': - resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5': - resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==} + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7': + resolution: {integrity: sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7': + resolution: {integrity: sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1': - resolution: {integrity: sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==} + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7': + resolution: {integrity: sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1': - resolution: {integrity: sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==} + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.7': + resolution: {integrity: sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1': - resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==} + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.29.7': + resolution: {integrity: sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.13.0 - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6': - resolution: {integrity: sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==} + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.29.7': + resolution: {integrity: sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-proposal-export-default-from@7.27.1': - resolution: {integrity: sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==} + '@babel/plugin-proposal-export-default-from@7.29.7': + resolution: {integrity: sha512-p+G5BNXDcy3bOXplhY4HybQ1GxH3i2Tppmdm/3epyRu2VgJJZuUlZ61MqRTg582Q7ZLBdP7fePYvsumSEkMxcQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -425,26 +467,26 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-export-default-from@7.28.6': - resolution: {integrity: sha512-Svlx1fjJFnNz0LZeUaybRukSxZI3KkpApUmIRzEdXC5k8ErTOz0OD0kNrICi5Vc3GlpP5ZCeRyRO+mfWTSz+iQ==} + '@babel/plugin-syntax-export-default-from@7.29.7': + resolution: {integrity: sha512-foag0BB37ROhdeIX9O8G0jX7hw0UekJc04cHMrYLOnrErsnBKqJGHJ8eDRpoCFZBvEPPygmmtw4qyU97qa4oOw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-flow@7.28.6': - resolution: {integrity: sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==} + '@babel/plugin-syntax-flow@7.29.7': + resolution: {integrity: sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-assertions@7.28.6': - resolution: {integrity: sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==} + '@babel/plugin-syntax-import-assertions@7.29.7': + resolution: {integrity: sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-attributes@7.28.6': - resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + '@babel/plugin-syntax-import-attributes@7.29.7': + resolution: {integrity: sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -459,8 +501,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-jsx@7.28.6': - resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -507,8 +549,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-typescript@7.28.6': - resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + '@babel/plugin-syntax-typescript@7.29.7': + resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -519,356 +561,356 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-transform-arrow-functions@7.27.1': - resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} + '@babel/plugin-transform-arrow-functions@7.29.7': + resolution: {integrity: sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-async-generator-functions@7.29.0': - resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==} + '@babel/plugin-transform-async-generator-functions@7.29.7': + resolution: {integrity: sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-async-to-generator@7.28.6': - resolution: {integrity: sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==} + '@babel/plugin-transform-async-to-generator@7.29.7': + resolution: {integrity: sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoped-functions@7.27.1': - resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==} + '@babel/plugin-transform-block-scoped-functions@7.29.7': + resolution: {integrity: sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoping@7.28.6': - resolution: {integrity: sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==} + '@babel/plugin-transform-block-scoping@7.29.7': + resolution: {integrity: sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-properties@7.28.6': - resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==} + '@babel/plugin-transform-class-properties@7.29.7': + resolution: {integrity: sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-static-block@7.28.6': - resolution: {integrity: sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==} + '@babel/plugin-transform-class-static-block@7.29.7': + resolution: {integrity: sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 - '@babel/plugin-transform-classes@7.28.6': - resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==} + '@babel/plugin-transform-classes@7.29.7': + resolution: {integrity: sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-computed-properties@7.28.6': - resolution: {integrity: sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==} + '@babel/plugin-transform-computed-properties@7.29.7': + resolution: {integrity: sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-destructuring@7.28.5': - resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==} + '@babel/plugin-transform-destructuring@7.29.7': + resolution: {integrity: sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-dotall-regex@7.28.6': - resolution: {integrity: sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==} + '@babel/plugin-transform-dotall-regex@7.29.7': + resolution: {integrity: sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-duplicate-keys@7.27.1': - resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==} + '@babel/plugin-transform-duplicate-keys@7.29.7': + resolution: {integrity: sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0': - resolution: {integrity: sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==} + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7': + resolution: {integrity: sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-transform-dynamic-import@7.27.1': - resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==} + '@babel/plugin-transform-dynamic-import@7.29.7': + resolution: {integrity: sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-explicit-resource-management@7.28.6': - resolution: {integrity: sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==} + '@babel/plugin-transform-explicit-resource-management@7.29.7': + resolution: {integrity: sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-exponentiation-operator@7.28.6': - resolution: {integrity: sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==} + '@babel/plugin-transform-exponentiation-operator@7.29.7': + resolution: {integrity: sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-export-namespace-from@7.27.1': - resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} + '@babel/plugin-transform-export-namespace-from@7.29.7': + resolution: {integrity: sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-flow-strip-types@7.27.1': - resolution: {integrity: sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==} + '@babel/plugin-transform-flow-strip-types@7.29.7': + resolution: {integrity: sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-for-of@7.27.1': - resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==} + '@babel/plugin-transform-for-of@7.29.7': + resolution: {integrity: sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-function-name@7.27.1': - resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==} + '@babel/plugin-transform-function-name@7.29.7': + resolution: {integrity: sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-json-strings@7.28.6': - resolution: {integrity: sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==} + '@babel/plugin-transform-json-strings@7.29.7': + resolution: {integrity: sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-literals@7.27.1': - resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==} + '@babel/plugin-transform-literals@7.29.7': + resolution: {integrity: sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-logical-assignment-operators@7.28.6': - resolution: {integrity: sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==} + '@babel/plugin-transform-logical-assignment-operators@7.29.7': + resolution: {integrity: sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-member-expression-literals@7.27.1': - resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==} + '@babel/plugin-transform-member-expression-literals@7.29.7': + resolution: {integrity: sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-amd@7.27.1': - resolution: {integrity: sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==} + '@babel/plugin-transform-modules-amd@7.29.7': + resolution: {integrity: sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-commonjs@7.28.6': - resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + '@babel/plugin-transform-modules-commonjs@7.29.7': + resolution: {integrity: sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-systemjs@7.29.0': - resolution: {integrity: sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==} + '@babel/plugin-transform-modules-systemjs@7.29.7': + resolution: {integrity: sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-umd@7.27.1': - resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==} + '@babel/plugin-transform-modules-umd@7.29.7': + resolution: {integrity: sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-named-capturing-groups-regex@7.29.0': - resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==} + '@babel/plugin-transform-named-capturing-groups-regex@7.29.7': + resolution: {integrity: sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-transform-new-target@7.27.1': - resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==} + '@babel/plugin-transform-new-target@7.29.7': + resolution: {integrity: sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-nullish-coalescing-operator@7.28.6': - resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==} + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7': + resolution: {integrity: sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-numeric-separator@7.28.6': - resolution: {integrity: sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==} + '@babel/plugin-transform-numeric-separator@7.29.7': + resolution: {integrity: sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-object-rest-spread@7.28.6': - resolution: {integrity: sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==} + '@babel/plugin-transform-object-rest-spread@7.29.7': + resolution: {integrity: sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-object-super@7.27.1': - resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==} + '@babel/plugin-transform-object-super@7.29.7': + resolution: {integrity: sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-optional-catch-binding@7.28.6': - resolution: {integrity: sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==} + '@babel/plugin-transform-optional-catch-binding@7.29.7': + resolution: {integrity: sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-optional-chaining@7.28.6': - resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==} + '@babel/plugin-transform-optional-chaining@7.29.7': + resolution: {integrity: sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-parameters@7.27.7': - resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==} + '@babel/plugin-transform-parameters@7.29.7': + resolution: {integrity: sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-methods@7.28.6': - resolution: {integrity: sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==} + '@babel/plugin-transform-private-methods@7.29.7': + resolution: {integrity: sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-property-in-object@7.28.6': - resolution: {integrity: sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==} + '@babel/plugin-transform-private-property-in-object@7.29.7': + resolution: {integrity: sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-property-literals@7.27.1': - resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==} + '@babel/plugin-transform-property-literals@7.29.7': + resolution: {integrity: sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-display-name@7.28.0': - resolution: {integrity: sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==} + '@babel/plugin-transform-react-display-name@7.29.7': + resolution: {integrity: sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-self@7.27.1': - resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-source@7.27.1': - resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx@7.28.6': - resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==} + '@babel/plugin-transform-react-jsx@7.29.7': + resolution: {integrity: sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-regenerator@7.29.0': - resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==} + '@babel/plugin-transform-regenerator@7.29.7': + resolution: {integrity: sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-regexp-modifiers@7.28.6': - resolution: {integrity: sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==} + '@babel/plugin-transform-regexp-modifiers@7.29.7': + resolution: {integrity: sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-transform-reserved-words@7.27.1': - resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==} + '@babel/plugin-transform-reserved-words@7.29.7': + resolution: {integrity: sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-runtime@7.29.0': - resolution: {integrity: sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==} + '@babel/plugin-transform-runtime@7.29.7': + resolution: {integrity: sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-shorthand-properties@7.27.1': - resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} + '@babel/plugin-transform-shorthand-properties@7.29.7': + resolution: {integrity: sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-spread@7.28.6': - resolution: {integrity: sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==} + '@babel/plugin-transform-spread@7.29.7': + resolution: {integrity: sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-sticky-regex@7.27.1': - resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==} + '@babel/plugin-transform-sticky-regex@7.29.7': + resolution: {integrity: sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-template-literals@7.27.1': - resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==} + '@babel/plugin-transform-template-literals@7.29.7': + resolution: {integrity: sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-typeof-symbol@7.27.1': - resolution: {integrity: sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==} + '@babel/plugin-transform-typeof-symbol@7.29.7': + resolution: {integrity: sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-typescript@7.28.6': - resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + '@babel/plugin-transform-typescript@7.29.7': + resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-unicode-escapes@7.27.1': - resolution: {integrity: sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==} + '@babel/plugin-transform-unicode-escapes@7.29.7': + resolution: {integrity: sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-unicode-property-regex@7.28.6': - resolution: {integrity: sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==} + '@babel/plugin-transform-unicode-property-regex@7.29.7': + resolution: {integrity: sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-unicode-regex@7.27.1': - resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==} + '@babel/plugin-transform-unicode-regex@7.29.7': + resolution: {integrity: sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-unicode-sets-regex@7.28.6': - resolution: {integrity: sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==} + '@babel/plugin-transform-unicode-sets-regex@7.29.7': + resolution: {integrity: sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/preset-env@7.29.0': - resolution: {integrity: sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==} + '@babel/preset-env@7.29.7': + resolution: {integrity: sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -878,26 +920,26 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 - '@babel/preset-typescript@7.28.5': - resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} + '@babel/preset-typescript@7.29.7': + resolution: {integrity: sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.28.6': - resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@0.2.3': @@ -907,14 +949,29 @@ packages: resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} engines: {node: '>=0.8.0'} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -925,8 +982,14 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -937,8 +1000,14 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -949,8 +1018,14 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -961,8 +1036,14 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -973,8 +1054,14 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -985,8 +1072,14 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -997,8 +1090,14 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -1009,8 +1108,14 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -1021,8 +1126,14 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -1033,8 +1144,14 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -1045,8 +1162,14 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -1057,8 +1180,14 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -1069,8 +1198,14 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -1081,8 +1216,14 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -1093,8 +1234,14 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -1105,14 +1252,26 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -1123,14 +1282,26 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -1141,14 +1312,26 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -1159,8 +1342,14 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -1171,8 +1360,14 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -1183,8 +1378,14 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -1195,8 +1396,14 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -1211,6 +1418,18 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/eslintrc@2.1.4': resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1219,6 +1438,14 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@fastify/accept-negotiator@2.0.1': resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} @@ -1261,6 +1488,9 @@ packages: '@fastify/proxy-addr@5.1.0': resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@fastify/rate-limit@10.3.0': + resolution: {integrity: sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==} + '@fastify/send@4.1.0': resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==} @@ -1294,6 +1524,18 @@ packages: '@hapi/topo@5.1.0': resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -1307,8 +1549,12 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead - '@ioredis/commands@1.5.1': - resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@ioredis/commands@1.10.0': + resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} '@isaacs/cliui@9.0.0': resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} @@ -1322,8 +1568,8 @@ packages: resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} engines: {node: '>=8'} '@jest/console@29.7.0': @@ -1419,6 +1665,12 @@ packages: resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} @@ -1434,14 +1686,17 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@package-json/types@0.0.12': + resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@prisma/client@6.19.2': - resolution: {integrity: sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==} + '@prisma/client@6.19.3': + resolution: {integrity: sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==} engines: {node: '>=18.18'} peerDependencies: prisma: '*' @@ -1452,23 +1707,23 @@ packages: typescript: optional: true - '@prisma/config@6.19.2': - resolution: {integrity: sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==} + '@prisma/config@6.19.3': + resolution: {integrity: sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==} - '@prisma/debug@6.19.2': - resolution: {integrity: sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==} + '@prisma/debug@6.19.3': + resolution: {integrity: sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==} '@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7': resolution: {integrity: sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==} - '@prisma/engines@6.19.2': - resolution: {integrity: sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==} + '@prisma/engines@6.19.3': + resolution: {integrity: sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==} - '@prisma/fetch-engine@6.19.2': - resolution: {integrity: sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==} + '@prisma/fetch-engine@6.19.3': + resolution: {integrity: sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==} - '@prisma/get-platform@6.19.2': - resolution: {integrity: sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==} + '@prisma/get-platform@6.19.3': + resolution: {integrity: sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==} '@react-native-async-storage/async-storage@2.2.0': resolution: {integrity: sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==} @@ -1617,25 +1872,25 @@ packages: '@types/react': optional: true - '@react-navigation/bottom-tabs@7.15.5': - resolution: {integrity: sha512-wQHredlCrRmShWQ1vF4HUcLdaiJ8fUgnbaeQH7BJ7MQVQh4mdzab0IOY/4QSmUyNRB350oyu1biTycyQ5FKWMQ==} + '@react-navigation/bottom-tabs@7.16.2': + resolution: {integrity: sha512-Lbp++BGMc7SQXnyKuO/JrQJIhFH0zyB5v4kIEbnzDJLJfgubd5hoSe+QfCqy4YHfLA4phC4Xf/6Q2Ic8x7datQ==} peerDependencies: - '@react-navigation/native': ^7.1.33 + '@react-navigation/native': ^7.2.5 react: '>= 18.2.0' react-native: '*' react-native-safe-area-context: '>= 4.0.0' react-native-screens: '>= 4.0.0' - '@react-navigation/core@7.16.1': - resolution: {integrity: sha512-xhquoyhKdqDfiL7LuupbwYnmauUGfVFGDEJO34m26k8zSN1eDjQ2stBZcHN8ILOI1PrG9885nf8ZmfaQxPS0ww==} + '@react-navigation/core@7.17.5': + resolution: {integrity: sha512-6fDCwDTWC7DJn0SDb9DJGRlipaygHIc+2elpZBJI6Crl/2Pu+Z1d6W4jMJ2gZO6iHKf+Pe5sUiQ/uwepGprZtg==} peerDependencies: react: '>= 18.2.0' - '@react-navigation/elements@2.9.10': - resolution: {integrity: sha512-N8tuBekzTRb0pkMHFJGvmC6Q5OisSbt6gzvw7RHMnp4NDo5auVllT12sWFaTXf8mTduaLKNSrD/NZNaOqThCBg==} + '@react-navigation/elements@2.9.19': + resolution: {integrity: sha512-gBUvCZuUkOGw1KpLQEZIkByUz8RYPwXeoA6mZFJy9K1mxd8GdqHDMFCIoB0lfPz9rgrHj99RvtdlGZ/ZzkZv2A==} peerDependencies: '@react-native-masked-view/masked-view': '>= 0.2.0' - '@react-navigation/native': ^7.1.33 + '@react-navigation/native': ^7.2.5 react: '>= 18.2.0' react-native: '*' react-native-safe-area-context: '>= 4.0.0' @@ -1643,159 +1898,159 @@ packages: '@react-native-masked-view/masked-view': optional: true - '@react-navigation/native-stack@7.14.4': - resolution: {integrity: sha512-HFEnM5Q7JY3FmmiolD/zvgY+9sxZAyVGPZJoz7BdTvJmi1VHOdplf24YiH45mqeitlGnaOlvNT55rH4abHJ5eA==} + '@react-navigation/native-stack@7.16.0': + resolution: {integrity: sha512-wM21rHYR2XifjDnKLrr3HeHUeGsWQZJRwPqEzy1Vp/a9k3ieiwTGpmpDItD/jtERH9qkYESwDPO6oEtrVBEpQg==} peerDependencies: - '@react-navigation/native': ^7.1.33 + '@react-navigation/native': ^7.2.5 react: '>= 18.2.0' react-native: '*' react-native-safe-area-context: '>= 4.0.0' react-native-screens: '>= 4.0.0' - '@react-navigation/native@7.1.33': - resolution: {integrity: sha512-DpFdWGcgLajKZ1TuIvDNQsblN2QaUFWpTQaB8v7WRP9Mix8H/6TFoIrZd93pbymI2hybd6UYrD+lI408eWVcfw==} + '@react-navigation/native@7.2.5': + resolution: {integrity: sha512-01AAUQiiHQAfTabq+ZyU1/ZWq+AbB/J3v0CB0UTJSON6M6cuadWNsbChzrZUdqQvHrXvg96U5i2PQLJzK3+zpg==} peerDependencies: react: '>= 18.2.0' react-native: '*' - '@react-navigation/routers@7.5.3': - resolution: {integrity: sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==} + '@react-navigation/routers@7.5.5': + resolution: {integrity: sha512-9/hhMte12Kgu+pMnLfA4EWJ0OQmIEAMVMX06FPH2yGkEQSQ3JhhCN/GkcRikzQhtEi97VYYQA15umptBUShcOQ==} - '@rollup/rollup-android-arm-eabi@4.59.0': - resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.59.0': - resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.59.0': - resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.59.0': - resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.59.0': - resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.59.0': - resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.59.0': - resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.59.0': - resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.59.0': - resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.59.0': - resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.59.0': - resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.59.0': - resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.59.0': - resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.59.0': - resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.59.0': - resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.59.0': - resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.59.0': - resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.59.0': - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.59.0': - resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.59.0': - resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.59.0': - resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} cpu: [x64] os: [win32] @@ -1820,8 +2075,8 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@sveltejs/acorn-typescript@1.0.9': - resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} + '@sveltejs/acorn-typescript@1.0.10': + resolution: {integrity: sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==} peerDependencies: acorn: ^8.9.0 @@ -1830,15 +2085,15 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/kit@2.54.0': - resolution: {integrity: sha512-WDJApQ1ipZLbaC4YjqJjwYR9y7QQgTqVwEObgNZ8Mu/eVQJqn4Qzw9a+n7mr5xnBYiAYz9UdJOOl+aqVbfGXcA==} + '@sveltejs/kit@2.61.1': + resolution: {integrity: sha512-Ny8s1SR1TyQS2hD2Rvw0XKzU2Nw1eUF52dTb6T2bdcgz7wSC+Nyb5IwjWYlR4b2dvbbR5NJDiQwHg3rnNseghg==} engines: {node: '>=18.13'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.0.0 '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: ^5.3.3 + typescript: ^5.3.3 || ^6.0.0 vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 peerDependenciesMeta: '@opentelemetry/api': @@ -1861,6 +2116,9 @@ packages: svelte: ^5.0.0 vite: ^6.3.0 || ^7.0.0 + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1876,9 +2134,15 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -1897,8 +2161,11 @@ packages: '@types/jest@29.5.14': resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} - '@types/node@22.19.15': - resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@22.19.19': + resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==} '@types/qrcode@1.5.6': resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} @@ -1912,8 +2179,8 @@ packages: '@types/react-test-renderer@19.1.0': resolution: {integrity: sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==} - '@types/react@19.2.14': - resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/react@19.2.15': + resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -1927,67 +2194,187 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.57.0': - resolution: {integrity: sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==} + '@typescript-eslint/eslint-plugin@8.60.0': + resolution: {integrity: sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.57.0 + '@typescript-eslint/parser': ^8.60.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.57.0': - resolution: {integrity: sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==} + '@typescript-eslint/parser@8.60.0': + resolution: {integrity: sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.57.0': - resolution: {integrity: sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==} + '@typescript-eslint/project-service@8.60.0': + resolution: {integrity: sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.57.0': - resolution: {integrity: sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==} + '@typescript-eslint/scope-manager@8.60.0': + resolution: {integrity: sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.57.0': - resolution: {integrity: sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==} + '@typescript-eslint/tsconfig-utils@8.60.0': + resolution: {integrity: sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.57.0': - resolution: {integrity: sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==} + '@typescript-eslint/type-utils@8.60.0': + resolution: {integrity: sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.57.0': - resolution: {integrity: sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==} + '@typescript-eslint/types@8.60.0': + resolution: {integrity: sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.57.0': - resolution: {integrity: sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==} + '@typescript-eslint/typescript-estree@8.60.0': + resolution: {integrity: sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.57.0': - resolution: {integrity: sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==} + '@typescript-eslint/utils@8.60.0': + resolution: {integrity: sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.57.0': - resolution: {integrity: sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==} + '@typescript-eslint/visitor-keys@8.60.0': + resolution: {integrity: sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@ungap/structured-clone@1.3.1': + resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} + cpu: [x64] + os: [win32] '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} @@ -2058,11 +2445,11 @@ packages: ajv: optional: true - ajv@6.14.0: - resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - ajv@8.18.0: - resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} anser@1.4.10: resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} @@ -2189,8 +2576,8 @@ packages: resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - babel-plugin-polyfill-corejs2@0.4.16: - resolution: {integrity: sha512-xaVwwSfebXf0ooE11BJovZYKhFjIvQo7TsyVpETuIeH2JHv0k/T6Y5j22pPTvqYqmpkxdlPAJlyJ0tfOJAoMxw==} + babel-plugin-polyfill-corejs2@0.4.17: + resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 @@ -2199,13 +2586,13 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-plugin-polyfill-corejs3@0.14.1: - resolution: {integrity: sha512-ENp89vM9Pw4kv/koBb5N2f9bDZsR0hpf3BdPMOg/pkS3pwO4dzNnQZVXtBbeyAadgm865DmQG2jMMLqmZXvuCw==} + babel-plugin-polyfill-corejs3@0.14.2: + resolution: {integrity: sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-plugin-polyfill-regenerator@0.6.7: - resolution: {integrity: sha512-OTYbUlSwXhNgr4g6efMZgsO8//jA61P7ZbRX3iTT53VON8l+WQS8IAUEVo4a4cWknrg2W8Cj4gQhRYNCJ8GkAA==} + babel-plugin-polyfill-regenerator@0.6.8: + resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 @@ -2233,11 +2620,15 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.0: - resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + baseline-browser-mapping@2.10.33: + resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} engines: {node: '>=6.0.0'} hasBin: true @@ -2247,26 +2638,26 @@ packages: bn.js@4.12.3: resolution: {integrity: sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==} - body-parser@1.20.4: - resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + body-parser@1.20.5: + resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@1.1.15: + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} - brace-expansion@5.0.4: - resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2279,6 +2670,10 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + builtin-modules@5.2.0: + resolution: {integrity: sha512-02yxLeyxF4dNl6SlY6/5HfRSrSdZ/sCPoxy2kZNP5dZZX8LSAD9aE2gtJIUgWrsQTiMPl3mxESyrobSwvRGisQ==} + engines: {node: '>=18.20'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -2299,8 +2694,8 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - call-bind@1.0.8: - resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} engines: {node: '>= 0.4'} call-bound@1.0.4: @@ -2319,8 +2714,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001778: - resolution: {integrity: sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==} + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} @@ -2330,6 +2725,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -2357,15 +2755,23 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} - citty@0.2.1: - resolution: {integrity: sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==} + citty@0.2.2: + resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + clean-regexp@1.0.0: + resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} + engines: {node: '>=4'} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -2392,8 +2798,8 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - cluster-key-slot@1.1.2: - resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + cluster-key-slot@1.1.1: + resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} engines: {node: '>=0.10.0'} co@4.6.0: @@ -2443,6 +2849,10 @@ packages: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} + comment-parser@1.4.7: + resolution: {integrity: sha512-0h+uSNtQGW3D98eQt3jJ8L06Fves8hncB4V/PKdw/Qb8Hnk19VaKuTr55UNRYiSoVa7WwrFls+rh3ux9agmkeQ==} + engines: {node: '>= 12.0.0'} + compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} @@ -2489,8 +2899,8 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} - core-js-compat@3.48.0: - resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} + core-js-compat@3.49.0: + resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} cosmiconfig@9.0.1: resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} @@ -2516,6 +2926,9 @@ packages: css-in-js-utils@3.1.0: resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} @@ -2545,8 +2958,8 @@ packages: dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} - dayjs@1.11.20: - resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} @@ -2607,8 +3020,8 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} @@ -2633,8 +3046,8 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} - devalue@5.6.4: - resolution: {integrity: sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==} + devalue@5.8.1: + resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} @@ -2678,11 +3091,11 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - effect@3.18.4: - resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==} + effect@3.21.0: + resolution: {integrity: sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==} - electron-to-chromium@1.5.313: - resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==} + electron-to-chromium@1.5.364: + resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} @@ -2706,6 +3119,10 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.22.1: + resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} + engines: {node: '>=10.13.0'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -2729,8 +3146,8 @@ packages: resolution: {integrity: sha512-kNAL7hESndBCrWwS72QyV3IVOTrVmj9D062FV5BQswNL5zEdeRmz/WJFyh6Aj/plvvSOrzddkxW57HgkZcR9Fw==} engines: {node: '>= 0.8'} - es-abstract@1.24.1: - resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} engines: {node: '>= 0.4'} es-define-property@1.0.1: @@ -2741,15 +3158,15 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-iterator-helpers@1.3.0: - resolution: {integrity: sha512-04cg8iJFDOxWcYlu0GFFWgs7vtaEPCmr5w1nrj9V3z3axu/48HCMwK6VMp45Zh3ZB+xLP1ifbJfrq86+1ypKKQ==} + es-iterator-helpers@1.3.2: + resolution: {integrity: sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==} engines: {node: '>= 0.4'} es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} es-set-tostringtag@2.1.0: @@ -2769,8 +3186,13 @@ packages: engines: {node: '>=12'} hasBin: true - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} engines: {node: '>=18'} hasBin: true @@ -2793,12 +3215,46 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + eslint-compat-utils@0.5.1: + resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + eslint-config-prettier@8.10.2: resolution: {integrity: sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==} hasBin: true peerDependencies: eslint: '>=7.0.0' + eslint-import-context@0.1.9: + resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + peerDependencies: + unrs-resolver: ^1.0.0 + peerDependenciesMeta: + unrs-resolver: + optional: true + + eslint-import-resolver-typescript@4.4.4: + resolution: {integrity: sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==} + engines: {node: ^16.17.0 || >=18.6.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-plugin-es-x@7.8.0: + resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '>=8' + eslint-plugin-eslint-comments@3.2.0: resolution: {integrity: sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==} engines: {node: '>=6.5.0'} @@ -2812,14 +3268,27 @@ packages: '@babel/eslint-parser': ^7.12.0 eslint: ^8.1.0 - eslint-plugin-jest@29.15.0: - resolution: {integrity: sha512-ZCGr7vTH2WSo2hrK5oM2RULFmMruQ7W3cX7YfwoTiPfzTGTFBMmrVIz45jZHd++cGKj/kWf02li/RhTGcANJSA==} + eslint-plugin-import-x@4.16.2: + resolution: {integrity: sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/utils': ^8.56.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + eslint-import-resolver-node: '*' + peerDependenciesMeta: + '@typescript-eslint/utils': + optional: true + eslint-import-resolver-node: + optional: true + + eslint-plugin-jest@29.15.2: + resolution: {integrity: sha512-kEN4r9RZl1xcsb4arGq89LrcVdOUFII/JSCwtTPJyv16mDwmPrcuEQwpxqZHeINvcsd7oK5O/rhdGlxFRaZwvQ==} engines: {node: ^20.12.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@typescript-eslint/eslint-plugin': ^8.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 jest: '*' - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <7.0.0' peerDependenciesMeta: '@typescript-eslint/eslint-plugin': optional: true @@ -2828,11 +3297,30 @@ packages: typescript: optional: true - eslint-plugin-react-hooks@7.0.1: - resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} + eslint-plugin-n@18.0.1: + resolution: {integrity: sha512-q3ARhk+eZRc7myR0KHx+R3/GJeOHF+Ir6PK95Pu2tEX8Sl/4BIpmmVLva2kPrjC2gCmn6WHlHm+3yeo6Rxhycw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: '>=8.57.1' + ts-declaration-location: ^1.0.6 + typescript: '>=5.0.0' + peerDependenciesMeta: + ts-declaration-location: + optional: true + typescript: + optional: true + + eslint-plugin-promise@7.3.0: + resolution: {integrity: sha512-6uGiOR0INuujr6PEQmeSSP7GbIMJ/ebEXXiEzb/nOj68LknH5Pxzb/AbZivmr6VE6TkTE8rTjRK9zhKpK6HsRA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} engines: {node: '>=18'} peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 eslint-plugin-react-native-globals@0.1.2: resolution: {integrity: sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g==} @@ -2848,6 +3336,16 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + eslint-plugin-security@4.0.0: + resolution: {integrity: sha512-tfuQT8K/Li1ZxhFzyD8wPIKtlzZxqBcPr9q0jFMQ77wWAbKBVEhaMPVQRTMTvCMUDhwBe5vPVqQPwAGk/ASfxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-plugin-unicorn@64.0.0: + resolution: {integrity: sha512-rNZwalHh8i0UfPlhNwg5BTUO1CMdKNmjqe+TgzOTZnpKoi8VBgsW7u9qCHIdpxEzZ1uwrJrPF0uRb7l//K38gA==} + engines: {node: ^20.10.0 || >=21.0.0} + peerDependencies: + eslint: '>=9.38.0' + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -2856,6 +3354,10 @@ packages: resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint-visitor-keys@2.1.0: resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} engines: {node: '>=10'} @@ -2868,6 +3370,16 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@10.4.1: + resolution: {integrity: sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + eslint@8.57.1: resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2877,6 +3389,10 @@ packages: esm-env@1.2.2: resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2890,8 +3406,13 @@ packages: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} - esrap@2.2.3: - resolution: {integrity: sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==} + esrap@2.2.9: + resolution: {integrity: sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -2946,8 +3467,8 @@ packages: resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} engines: {node: '>=8.0.0'} - fast-copy@4.0.2: - resolution: {integrity: sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==} + fast-copy@4.0.3: + resolution: {integrity: sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw==} fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} @@ -2962,8 +3483,8 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - fast-json-stringify@6.3.0: - resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} + fast-json-stringify@6.4.0: + resolution: {integrity: sha512-ibRCQ0GZKJIQ+P3Et1h0LhPgp3PMTYk0MH8O+kW3lNYsvmaQww5Nn3f1jf73Q0jR1Yz3a1CDP4/NZD3vOajWJQ==} fast-jwt@5.0.6: resolution: {integrity: sha512-LPE7OCGUl11q3ZgW681cEU2d0d2JZ37hhJAmetCgNyW8waVaJVZXhyFF6U2so1Iim58Yc7pfxJe2P7MNetQH2g==} @@ -2978,11 +3499,11 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} - fast-xml-parser@4.5.4: - resolution: {integrity: sha512-jE8ugADnYOBsu1uaoayVl1tVKAMNOXyjwvv2U6udEA2ORBhDooJDWoGxTkhd4Qn4yh59JVVt/pKXtjPwx9OguQ==} + fast-xml-parser@4.5.6: + resolution: {integrity: sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==} hasBin: true fastfall@1.5.1: @@ -2992,8 +3513,8 @@ packages: fastify-plugin@5.1.0: resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} - fastify@5.8.2: - resolution: {integrity: sha512-lZmt3navvZG915IE+f7/TIVamxIwmBd+OMB+O9WBzcpIwOo6F0LTh0sluoMFk5VkrKTvvrwIaoJPkir4Z+jtAg==} + fastify@5.8.5: + resolution: {integrity: sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==} fastparallel@2.4.1: resolution: {integrity: sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==} @@ -3031,6 +3552,10 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -3043,10 +3568,14 @@ packages: resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} engines: {node: '>= 0.8'} - find-my-way@9.5.0: - resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} + find-my-way@9.6.0: + resolution: {integrity: sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==} engines: {node: '>=20'} + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + engines: {node: '>=18'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -3059,8 +3588,12 @@ packages: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} - flatted@3.4.1: - resolution: {integrity: sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} flow-enums-runtime@0.0.6: resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==} @@ -3131,8 +3664,8 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-tsconfig@4.13.6: - resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} giget@2.0.0: resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} @@ -3160,10 +3693,21 @@ packages: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -3197,8 +3741,8 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} helmet@7.2.0: @@ -3217,8 +3761,8 @@ packages: hermes-estree@0.32.0: resolution: {integrity: sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==} - hermes-estree@0.33.3: - resolution: {integrity: sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==} + hermes-estree@0.35.0: + resolution: {integrity: sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==} hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} @@ -3226,8 +3770,8 @@ packages: hermes-parser@0.32.0: resolution: {integrity: sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==} - hermes-parser@0.33.3: - resolution: {integrity: sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==} + hermes-parser@0.35.0: + resolution: {integrity: sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==} hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -3235,6 +3779,10 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -3283,6 +3831,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -3300,12 +3852,12 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - ioredis@5.10.0: - resolution: {integrity: sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==} + ioredis@5.11.0: + resolution: {integrity: sha512-EZBErytyVovD8f6pDfG3Kb37N6Y3lmDA9NNj+4+IP13CzzHGeX+OyeRM2Um13khRzoBSzzL+5lVnCX8V2RLeMg==} engines: {node: '>=12.22.0'} - ipaddr.js@2.3.0: - resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + ipaddr.js@2.4.0: + resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==} engines: {node: '>= 10'} is-array-buffer@3.0.5: @@ -3330,12 +3882,19 @@ packages: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} + is-builtin-module@5.0.0: + resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} + engines: {node: '>=18.20'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} is-data-view@1.0.2: @@ -3629,8 +4188,8 @@ packages: node-notifier: optional: true - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true joi@17.13.3: @@ -3700,8 +4259,8 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - launch-editor@2.13.1: - resolution: {integrity: sha512-lPSddlAAluRKJ7/cjRFoXUFzaX7q/YKI7yPHuEvSJVqoXvFnJov1/Ud87Aa4zULIbA9Nja4mSPK8l0z/7eV2wA==} + launch-editor@2.14.0: + resolution: {integrity: sha512-Pj3ZOx9dD1BClS7YcSQx0An1PCF9wz4JpvbEmKvDxQtm0jxlkk5NhW8x0SBAKA/acHBKZaqdd5FFOWlXo500JA==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -3734,20 +4293,14 @@ packages: lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - lodash.defaults@4.2.0: - resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - - lodash.isarguments@3.1.0: - resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} - lodash@4.17.23: - resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} @@ -3764,8 +4317,8 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lru-cache@11.2.6: - resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -3812,61 +4365,61 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - metro-babel-transformer@0.83.5: - resolution: {integrity: sha512-d9FfmgUEVejTiSb7bkQeLRGl6aeno2UpuPm3bo3rCYwxewj03ymvOn8s8vnS4fBqAPQ+cE9iQM40wh7nGXR+eA==} + metro-babel-transformer@0.83.7: + resolution: {integrity: sha512-sBqBkt6kNut/88bv+Ucvm4yqdPetbvAEsHzi3MAgJEifOSYYzX5Z5Kgw3TFOrwf/mHJTOBG2ONlaMHoyfP15TA==} engines: {node: '>=20.19.4'} - metro-cache-key@0.83.5: - resolution: {integrity: sha512-Ycl8PBajB7bhbAI7Rt0xEyiF8oJ0RWX8EKkolV1KfCUlC++V/GStMSGpPLwnnBZXZWkCC5edBPzv1Hz1Yi0Euw==} + metro-cache-key@0.83.7: + resolution: {integrity: sha512-W1c2Nmx8MiJTJt+eWhMO08z9VKi3kZOaz99IYGdqeqDgY9j+yZjXl62rUav4Di0heZfh4/n2s722PqRL1OODeg==} engines: {node: '>=20.19.4'} - metro-cache@0.83.5: - resolution: {integrity: sha512-oH+s4U+IfZyg8J42bne2Skc90rcuESIYf86dYittcdWQtPfcaFXWpByPyTuWk3rR1Zz3Eh5HOrcVImfEhhJLng==} + metro-cache@0.83.7: + resolution: {integrity: sha512-E9SRePXQ1Zvlj79VcOk57q7VC7rMHMFQ+jhmPHBiq+dJ0bJB5BL87lWZF6oh5X76Cci5tpDuQNaDwwuSCToEeg==} engines: {node: '>=20.19.4'} - metro-config@0.83.5: - resolution: {integrity: sha512-JQ/PAASXH7yczgV6OCUSRhZYME+NU8NYjI2RcaG5ga4QfQ3T/XdiLzpSb3awWZYlDCcQb36l4Vl7i0Zw7/Tf9w==} + metro-config@0.83.7: + resolution: {integrity: sha512-83mjWFbFOt2GeJ6pFIum5mSnc1uTsZJAtD8o4ej0s4NVsYsA7fB+pHvTfHhFrpeMONaobu2riKavkPei05Er/Q==} engines: {node: '>=20.19.4'} - metro-core@0.83.5: - resolution: {integrity: sha512-YcVcLCrf0ed4mdLa82Qob0VxYqfhmlRxUS8+TO4gosZo/gLwSvtdeOjc/Vt0pe/lvMNrBap9LlmvZM8FIsMgJQ==} + metro-core@0.83.7: + resolution: {integrity: sha512-6yn3w1wnltT6RQl7p7YES2l95ArC+mWrOssEiH8p5/DDrJS65/szf9LsC9JrBv8c5DdvSY3V3f0GRYg0Ox7hCg==} engines: {node: '>=20.19.4'} - metro-file-map@0.83.5: - resolution: {integrity: sha512-ZEt8s3a1cnYbn40nyCD+CsZdYSlwtFh2kFym4lo+uvfM+UMMH+r/BsrC6rbNClSrt+B7rU9T+Te/sh/NL8ZZKQ==} + metro-file-map@0.83.7: + resolution: {integrity: sha512-+j0F1m+FQYVAQ6syf+mwhIPV5GoFQrkInX8bppuc50IzNsZbMrp8R5H/Sx/K2daQ3YEa9F/XwkeZT8gzJfgeCw==} engines: {node: '>=20.19.4'} - metro-minify-terser@0.83.5: - resolution: {integrity: sha512-Toe4Md1wS1PBqbvB0cFxBzKEVyyuYTUb0sgifAZh/mSvLH84qA1NAWik9sISWatzvfWf3rOGoUoO5E3f193a3Q==} + metro-minify-terser@0.83.7: + resolution: {integrity: sha512-MfJar2IS4tBRuLb9svwb0Gu5l9BsH+pcRm8eGcEi/wy8MzZinfinh5dFLt2nWkocnulIgtGB5NkFDdbXqMXKhQ==} engines: {node: '>=20.19.4'} - metro-resolver@0.83.5: - resolution: {integrity: sha512-7p3GtzVUpbAweJeCcUJihJeOQl1bDuimO5ueo1K0BUpUtR41q5EilbQ3klt16UTPPMpA+tISWBtsrqU556mY1A==} + metro-resolver@0.83.7: + resolution: {integrity: sha512-WSJIENlMcoSsuz66IfBHOkgfp3KJt2UW2TnEHPf1b8pIG2eEXNOVmo2+03A0H17WY2XGXWgxL0CG7FAopqgB1A==} engines: {node: '>=20.19.4'} - metro-runtime@0.83.5: - resolution: {integrity: sha512-f+b3ue9AWTVlZe2Xrki6TAoFtKIqw30jwfk7GQ1rDUBQaE0ZQ+NkiMEtb9uwH7uAjJ87U7Tdx1Jg1OJqUfEVlA==} + metro-runtime@0.83.7: + resolution: {integrity: sha512-9GKkJURaB2iyYoEExKnedzAHzxmKtSi+k0tsZUvMoU27tBZJElchYt7JH/Ai/XzYAI9lCAaV7u5HZSI8J5Z+wQ==} engines: {node: '>=20.19.4'} - metro-source-map@0.83.5: - resolution: {integrity: sha512-VT9bb2KO2/4tWY9Z2yeZqTUao7CicKAOps9LUg2aQzsz+04QyuXL3qgf1cLUVRjA/D6G5u1RJAlN1w9VNHtODQ==} + metro-source-map@0.83.7: + resolution: {integrity: sha512-JgA1h7oc1a1jydBe1GhVFsUoMYo3wLPk7oRA32rjlDsq+sP2JLt9x2p2lWbNSxTm/u8NV4VRid3hvEJgcX8tKw==} engines: {node: '>=20.19.4'} - metro-symbolicate@0.83.5: - resolution: {integrity: sha512-EMIkrjNRz/hF+p0RDdxoE60+dkaTLPN3vaaGkFmX5lvFdO6HPfHA/Ywznzkev+za0VhPQ5KSdz49/MALBRteHA==} + metro-symbolicate@0.83.7: + resolution: {integrity: sha512-g4suyxw20WOHWI680c+Kq4wC/NF+Hx5pRH9afrMp+sMTxqLeKcPR1Xf4wMhsjlbvx7LbIREdke6q928jEjvJWw==} engines: {node: '>=20.19.4'} hasBin: true - metro-transform-plugins@0.83.5: - resolution: {integrity: sha512-KxYKzZL+lt3Os5H2nx7YkbkWVduLZL5kPrE/Yq+Prm/DE1VLhpfnO6HtPs8vimYFKOa58ncl60GpoX0h7Wm0Vw==} + metro-transform-plugins@0.83.7: + resolution: {integrity: sha512-Ss0FpBiZDjX2kwhukMDl5sNdYK8T/06IPqxNE4H6PTlRlfs9q11cef13c/xESY/Pm4VCkp1yJUZO3kXzvMxQFA==} engines: {node: '>=20.19.4'} - metro-transform-worker@0.83.5: - resolution: {integrity: sha512-8N4pjkNXc6ytlP9oAM6MwqkvUepNSW39LKYl9NjUMpRDazBQ7oBpQDc8Sz4aI8jnH6AGhF7s1m/ayxkN1t04yA==} + metro-transform-worker@0.83.7: + resolution: {integrity: sha512-UegCo7ygB2fT64mRK2nbAjQVJ1zSwIIHy8d96jJv2nKZFDaViYBiughEdu5HM/Ceq0WN3LZrZk3zhl9aoiLYFw==} engines: {node: '>=20.19.4'} - metro@0.83.5: - resolution: {integrity: sha512-BgsXevY1MBac/3ZYv/RfNFf/4iuW9X7f4H8ZNkiH+r667HD9sVujxcmu4jvEzGCAm4/WyKdZCuyhAcyhTHOucQ==} + metro@0.83.7: + resolution: {integrity: sha512-SPaPEyvTsTmd0LpT7RaZciQyDw2i/JB7+iY9L5VfBo72+psescFxBqpI1TL9dnL+pmnfkU+l/J1mEEGLeF65EQ==} engines: {node: '>=20.19.4'} hasBin: true @@ -3912,8 +4465,8 @@ packages: minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - minimatch@10.2.4: - resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} minimatch@3.1.5: @@ -3934,8 +4487,8 @@ packages: mnemonist@0.40.0: resolution: {integrity: sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==} - mnemonist@0.40.3: - resolution: {integrity: sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==} + mnemonist@0.40.4: + resolution: {integrity: sha512-ZAv+KNavneRVzu4tUeOgzkScI3W5BGwZ3rkxIpKtzzVgfTtWQFN1CgX0U72cyvyh3iTuHL3SiSmrQxTlryEIcw==} mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} @@ -3951,11 +4504,16 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -3994,8 +4552,9 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.36: - resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} node-stream-zip@1.15.0: resolution: {integrity: sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==} @@ -4015,13 +4574,13 @@ packages: nullthrows@1.1.1: resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} - nypm@0.6.5: - resolution: {integrity: sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==} + nypm@0.6.6: + resolution: {integrity: sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==} engines: {node: '>=18'} hasBin: true - ob1@0.83.5: - resolution: {integrity: sha512-vNKPYC8L5ycVANANpF/S+WZHpfnRWKx/F3AYP4QMn6ZJTh+l2HOrId0clNkEmua58NB9vmI9Qh7YOoV/4folYg==} + ob1@0.83.7: + resolution: {integrity: sha512-9M5kpuOLyTPogMtZiQUIxdAZxl7Dxs6tVBbJErSumsqGMuhVSoUbkfeZ3XNPpLpwBBtqY5QDUzGwggLHX3slQg==} engines: {node: '>=20.19.4'} object-assign@4.1.1: @@ -4174,12 +4733,12 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} pino-abstract-transport@3.0.0: @@ -4204,8 +4763,12 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} - pkg-types@2.3.0: - resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} @@ -4218,8 +4781,8 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -4235,8 +4798,8 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - prisma@6.19.2: - resolution: {integrity: sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==} + prisma@6.19.3: + resolution: {integrity: sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==} engines: {node: '>=18.18'} hasBin: true peerDependencies: @@ -4279,8 +4842,8 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - qs@6.14.2: - resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} query-string@7.1.3: @@ -4310,10 +4873,10 @@ packages: react-devtools-core@6.1.5: resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==} - react-dom@19.2.4: - resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} peerDependencies: - react: ^19.2.4 + react: ^19.2.6 react-freeze@1.0.4: resolution: {integrity: sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==} @@ -4327,8 +4890,15 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-is@19.2.4: - resolution: {integrity: sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==} + react-is@19.2.6: + resolution: {integrity: sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==} + + react-native-camera-kit@14.2.0: + resolution: {integrity: sha512-rPk/4Ux52/Kc6oIPk0x6NsrvDkeL+kd/GAUJ4xBtTlnmiWjLTgeA2Vjgg9ik03mmyf6rV+LaqaOBT7KejhuHKQ==} + engines: {node: '>=18'} + peerDependencies: + react: '*' + react-native: '*' react-native-gesture-handler@2.31.2: resolution: {integrity: sha512-rw5q74i2AfS7YGYdbxQDhOU7xqgY6WRM1132/CCm3erqjblhECZDZFHIm0tteHoC9ih24wogVBVVzcTBQtZ+5A==} @@ -4356,20 +4926,20 @@ packages: react: '*' react-native: '*' - react-native-safe-area-context@5.7.0: - resolution: {integrity: sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ==} + react-native-safe-area-context@5.8.0: + resolution: {integrity: sha512-t+ZsAVzY/wWzzx34vqGbo3/as9EEESJdbyZNL7Yg5EYX+toYMtMqFoDDCvqZUi35eeGVsXc6pAaEk4edMwbuCQ==} peerDependencies: react: '*' react-native: '*' - react-native-screens@4.24.0: - resolution: {integrity: sha512-SyoiGaDofiyGPFrUkn1oGsAzkRuX1JUvTD9YQQK3G1JGQ5VWkvHgYSsc1K9OrLsDQxN7NmV71O0sHCAh8cBetA==} + react-native-screens@4.25.2: + resolution: {integrity: sha512-1Nj1fusFd+rIMKU/qC9yGKVG+3ofh11d3OdBQKL1iVvQfKvcB8vhvTGQf2TkfxW3bamxN+hCZIXmNuU0mRkyDg==} peerDependencies: react: '*' - react-native: '*' + react-native: '>=0.82.0' - react-native-svg@15.15.3: - resolution: {integrity: sha512-/k4KYwPBLGcx2f5d4FjE+vCScK7QOX14cl2lIASJ28u4slHHtIhL0SZKU7u9qmRBHxTCKPoPBtN6haT1NENJNA==} + react-native-svg@15.15.5: + resolution: {integrity: sha512-L4go5jA+GWutdJ/JucuN20cjAbMg1HmMtAP+wZ+3JLCf6Jd0bhXQHxciRP/AQm/FlrIEZwkMcHNZP+FXAiic0w==} peerDependencies: react: '*' react-native: '*' @@ -4379,6 +4949,13 @@ packages: deprecated: react-native-vector-icons package has moved to a new model of per-icon-family packages. See the https://github.com/oblador/react-native-vector-icons/blob/master/MIGRATION.md on how to migrate hasBin: true + react-native-view-shot@5.1.0: + resolution: {integrity: sha512-JZgElCD82aO+hejIF/leUzI7JufL9mgJ6ChzGWIcdZ2ajpaEvvSnvIcw0qD32XWkrbId8wfSbyz/4u/ulTQzQA==} + engines: {node: '>=20', npm: '>=10'} + peerDependencies: + react: '>=18.0.0' + react-native: '>=0.76.0' + react-native-web@0.21.2: resolution: {integrity: sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==} peerDependencies: @@ -4391,6 +4968,13 @@ packages: react: '*' react-native: '*' + react-native-worklets@0.5.1: + resolution: {integrity: sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==} + peerDependencies: + '@babel/core': ^7.0.0-0 + react: '*' + react-native: '*' + react-native@0.84.1: resolution: {integrity: sha512-0PjxOyXRu3tZ8EobabxSukvhKje2HJbsZikR0U+pvS0pYZza2hXKjcSBiBdFN4h9D0S3v6a8kkrDK6WTRKMwzg==} engines: {node: '>= 20.19.4'} @@ -4427,6 +5011,9 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} + real-require@1.0.0: + resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -4449,6 +5036,10 @@ packages: regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -4460,8 +5051,8 @@ packages: regjsgen@0.8.0: resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} - regjsparser@0.13.0: - resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} + regjsparser@0.13.1: + resolution: {integrity: sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==} hasBin: true require-directory@2.1.1: @@ -4494,13 +5085,13 @@ packages: resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} engines: {node: '>=10'} - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} hasBin: true - resolve@2.0.0-next.6: - resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} engines: {node: '>= 0.4'} hasBin: true @@ -4524,8 +5115,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rollup@4.59.0: - resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -4539,8 +5130,8 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} - safe-array-concat@1.1.3: - resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} safe-buffer@5.2.1: @@ -4554,8 +5145,12 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} - safe-regex2@5.0.0: - resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==} + safe-regex2@5.1.1: + resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} + hasBin: true + + safe-regex@2.1.1: + resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} @@ -4574,8 +5169,13 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} engines: {node: '>=10'} hasBin: true @@ -4597,8 +5197,8 @@ packages: set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} - set-cookie-parser@3.0.1: - resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} + set-cookie-parser@3.1.0: + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} @@ -4634,8 +5234,12 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + shell-quote@1.8.4: + resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} side-channel-map@1.0.1: @@ -4710,6 +5314,10 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stable-hash-x@0.2.0: + resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} + engines: {node: '>=12.0.0'} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -4798,6 +5406,10 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-indent@4.1.1: + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} + engines: {node: '>=12'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -4824,20 +5436,24 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte-check@4.4.5: - resolution: {integrity: sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==} + svelte-check@4.4.8: + resolution: {integrity: sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.0 typescript: '>=5.0.0' - svelte@5.53.10: - resolution: {integrity: sha512-UcNfWzbrjvYXYSk+U2hME25kpb87oq6/WVLeBF4khyQrb3Ob/URVlN23khal+RbdCUTMfg4qWjI9KZjCNFtYMQ==} + svelte@5.56.0: + resolution: {integrity: sha512-kTXr26t1bchFp28ROrb957LtbujpBmBDibmqMGziVpUs7awBi96TGgX6SovrA8BNoEUDVRK2Fb9FkeYlGspoVg==} engines: {node: '>=18'} - terser@5.46.0: - resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + terser@5.48.0: + resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} engines: {node: '>=10'} hasBin: true @@ -4849,11 +5465,14 @@ packages: resolution: {integrity: sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==} deprecated: no longer maintained + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - thread-stream@4.0.0: - resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + thread-stream@4.2.0: + resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} engines: {node: '>=20'} throat@5.0.0: @@ -4865,12 +5484,12 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + tinyexec@1.2.3: + resolution: {integrity: sha512-g62dB+w1/OEFnPvmX0yd/HnetYITOL+1nJW7kitOycOeAvmbWC/nu0fwmmQ/kupNojqExzyC/T++pST/jRJ2mQ==} engines: {node: '>=18'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} tinypool@1.1.1: @@ -4892,9 +5511,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - toad-cache@3.7.0: - resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} - engines: {node: '>=12'} + toad-cache@3.7.1: + resolution: {integrity: sha512-5DXWzE4Vz7xNHsv+xQ+MGfJYyC78Aok3tEr0MNwHoRf7vZnga1mQXZ4/Nsodld4VR6Wd+VhfmqnNrsRJyYPfrQ==} + engines: {node: '>=20'} toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} @@ -4911,8 +5530,8 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - ts-api-utils@2.4.0: - resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -4920,8 +5539,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.21.0: - resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + tsx@4.22.3: + resolution: {integrity: sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==} engines: {node: '>=18.0.0'} hasBin: true @@ -4961,10 +5580,17 @@ packages: resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} engines: {node: '>= 0.4'} - typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} engines: {node: '>= 0.4'} + typescript-eslint@8.60.0: + resolution: {integrity: sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -5005,6 +5631,9 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -5031,6 +5660,9 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + v8-to-istanbul@9.3.0: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} @@ -5075,8 +5707,8 @@ packages: terser: optional: true - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + vite@7.3.3: + resolution: {integrity: sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -5115,10 +5747,10 @@ packages: yaml: optional: true - vitefu@1.1.2: - resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==} + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: vite: optional: true @@ -5184,8 +5816,8 @@ packages: which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - which-typed-array@1.1.20: - resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + which-typed-array@1.1.21: + resolution: {integrity: sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw==} engines: {node: '>= 0.4'} which@2.0.2: @@ -5217,8 +5849,8 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - ws@6.2.3: - resolution: {integrity: sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==} + ws@6.2.4: + resolution: {integrity: sha512-PNIUUyLI5YpkJZj60YBzX1o0ByQ4ovvfmq9N/Kig/PAYbVlGyz4R6G0SEWrD0O9acc0sT2+IdMBVLFv8FSi0Nw==} peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ^5.0.2 @@ -5228,8 +5860,8 @@ packages: utf-8-validate: optional: true - ws@7.5.10: - resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + ws@7.5.11: + resolution: {integrity: sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==} engines: {node: '>=8.3.0'} peerDependencies: bufferutil: ^4.0.1 @@ -5254,8 +5886,8 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yaml@2.8.2: - resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} hasBin: true @@ -5301,25 +5933,25 @@ packages: snapshots: - '@babel/code-frame@7.29.0': + '@babel/code-frame@7.29.7': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.29.0': {} + '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.0': + '@babel/core@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.28.6 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -5329,805 +5961,814 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/eslint-parser@7.28.6(@babel/core@7.29.0)(eslint@8.57.1)': + '@babel/eslint-parser@7.29.7(@babel/core@7.29.7)(eslint@8.57.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.7 '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 eslint: 8.57.1 eslint-visitor-keys: 2.1.0 semver: 6.3.1 - '@babel/generator@7.29.1': + '@babel/generator@7.29.7': dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/helper-annotate-as-pure@7.27.3': + '@babel/helper-annotate-as-pure@7.29.7': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 - '@babel/helper-compilation-targets@7.28.6': + '@babel/helper-compilation-targets@7.29.7': dependencies: - '@babel/compat-data': 7.29.0 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.1 + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-member-expression-to-functions': 7.28.5 - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/traverse': 7.29.7 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0)': + '@babel/helper-create-regexp-features-plugin@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 regexpu-core: 6.4.0 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.7(@babel/core@7.29.0)': + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 debug: 4.4.3 lodash.debounce: 4.0.8 - resolve: 1.22.11 + resolve: 1.22.12 transitivePeerDependencies: - supports-color - '@babel/helper-globals@7.28.0': {} + '@babel/helper-globals@7.29.7': {} - '@babel/helper-member-expression-to-functions@7.28.5': + '@babel/helper-member-expression-to-functions@7.29.7': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.28.6': + '@babel/helper-module-imports@7.29.7': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-optimise-call-expression@7.27.1': + '@babel/helper-optimise-call-expression@7.29.7': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 - '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-plugin-utils@7.29.7': {} - '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)': + '@babel/helper-remap-async-to-generator@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-wrap-function': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-wrap-function': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-member-expression-to-functions': 7.28.5 - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.7': {} - '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} - '@babel/helper-validator-option@7.27.1': {} + '@babel/helper-validator-option@7.29.7': {} - '@babel/helper-wrap-function@7.28.6': + '@babel/helper-wrap-function@7.29.7': dependencies: - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helpers@7.28.6': + '@babel/helpers@7.29.7': dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 - '@babel/parser@7.29.0': + '@babel/parser@7.29.7': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0)': + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-export-default-from@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-export-default-from@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)': + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.7 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-export-default-from@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-export-default-from@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-flow@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-flow@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-import-assertions@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-arrow-functions@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-transform-async-generator-functions@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7) + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-async-to-generator@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-block-scoped-functions@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-block-scoping@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-class-properties@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-class-static-block@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-classes@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-globals': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-computed-properties@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/template': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/template': 7.29.7 - '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0)': + '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-dotall-regex@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-dotall-regex@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-duplicate-keys@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-dynamic-import@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-explicit-resource-management@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-exponentiation-operator@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-export-namespace-from@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-flow-strip-types@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.29.0) + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-flow': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-for-of@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-function-name@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-json-strings@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-literals@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-logical-assignment-operators@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-member-expression-literals@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-modules-amd@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-transform-modules-systemjs@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-modules-umd@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-transform-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-new-target@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-numeric-separator@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-object-rest-spread@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7) + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-object-super@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-optional-catch-binding@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-optional-chaining@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)': + '@babel/plugin-transform-parameters@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-private-methods@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-private-property-in-object@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-property-literals@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.29.0)': + '@babel/plugin-transform-react-display-name@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-react-jsx@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/types': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-transform-regenerator@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-regexp-modifiers@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-regexp-modifiers@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-reserved-words@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - babel-plugin-polyfill-corejs2: 0.4.16(@babel/core@7.29.0) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.0) - babel-plugin-polyfill-regenerator: 0.6.7(@babel/core@7.29.0) + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.7) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7) semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-shorthand-properties@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-spread@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-sticky-regex@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-template-literals@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-typeof-symbol@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-unicode-property-regex@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-unicode-sets-regex@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/preset-env@7.29.0(@babel/core@7.29.0)': - dependencies: - '@babel/compat-data': 7.29.0 - '@babel/core': 7.29.0 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.29.0) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0) - '@babel/plugin-syntax-import-assertions': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0) - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) - '@babel/plugin-transform-dotall-regex': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-explicit-resource-management': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-exponentiation-operator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-json-strings': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-modules-systemjs': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-regexp-modifiers': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-property-regex': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-sets-regex': 7.28.6(@babel/core@7.29.0) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.0) - babel-plugin-polyfill-corejs2: 0.4.16(@babel/core@7.29.0) - babel-plugin-polyfill-corejs3: 0.14.1(@babel/core@7.29.0) - babel-plugin-polyfill-regenerator: 0.6.7(@babel/core@7.29.0) - core-js-compat: 3.48.0 + '@babel/plugin-transform-unicode-escapes@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-unicode-property-regex@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-unicode-regex@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-unicode-sets-regex@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/preset-env@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/core': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7) + '@babel/plugin-syntax-import-assertions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.7) + '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-block-scoped-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-class-static-block': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-computed-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-dotall-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-duplicate-keys': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-dynamic-import': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-explicit-resource-management': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-exponentiation-operator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-export-namespace-from': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-function-name': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-json-strings': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-logical-assignment-operators': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-member-expression-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-amd': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-systemjs': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-umd': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-new-target': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-numeric-separator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-object-rest-spread': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-object-super': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-property-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-regenerator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-regexp-modifiers': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-reserved-words': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-shorthand-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-spread': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-sticky-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-typeof-symbol': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-escapes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-property-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-sets-regex': 7.29.7(@babel/core@7.29.7) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.7) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7) + babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.7) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7) + core-js-compat: 3.49.0 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.0)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/types': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/types': 7.29.7 esutils: 2.0.3 - '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': + '@babel/preset-typescript@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/runtime@7.28.6': {} + '@babel/runtime@7.29.7': {} - '@babel/template@7.28.6': + '@babel/template@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 - '@babel/traverse@7.29.0': + '@babel/traverse@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.29.0': + '@babel/types@7.29.7': dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 '@bcoe/v8-coverage@0.2.3': {} @@ -6135,153 +6776,252 @@ snapshots: dependencies: '@types/hammerjs': 2.0.46 + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.21.5': optional: true - '@esbuild/aix-ppc64@0.27.3': + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/aix-ppc64@0.28.0': optional: true '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-arm64@0.27.3': + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.28.0': optional: true '@esbuild/android-arm@0.21.5': optional: true - '@esbuild/android-arm@0.27.3': + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-arm@0.28.0': optional: true '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/android-x64@0.27.3': + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/android-x64@0.28.0': optional: true '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/darwin-arm64@0.27.3': + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.28.0': optional: true '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/darwin-x64@0.27.3': + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.28.0': optional: true '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.27.3': + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.28.0': optional: true '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/freebsd-x64@0.27.3': + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.28.0': optional: true '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/linux-arm64@0.27.3': + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.28.0': optional: true '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-arm@0.27.3': + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-arm@0.28.0': optional: true '@esbuild/linux-ia32@0.21.5': optional: true - '@esbuild/linux-ia32@0.27.3': + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.28.0': optional: true '@esbuild/linux-loong64@0.21.5': optional: true - '@esbuild/linux-loong64@0.27.3': + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.28.0': optional: true '@esbuild/linux-mips64el@0.21.5': optional: true - '@esbuild/linux-mips64el@0.27.3': + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.28.0': optional: true '@esbuild/linux-ppc64@0.21.5': optional: true - '@esbuild/linux-ppc64@0.27.3': + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.28.0': optional: true '@esbuild/linux-riscv64@0.21.5': optional: true - '@esbuild/linux-riscv64@0.27.3': + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.28.0': optional: true '@esbuild/linux-s390x@0.21.5': optional: true - '@esbuild/linux-s390x@0.27.3': + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.28.0': optional: true '@esbuild/linux-x64@0.21.5': optional: true - '@esbuild/linux-x64@0.27.3': + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/linux-x64@0.28.0': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.28.0': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.28.0': optional: true - '@esbuild/netbsd-arm64@0.27.3': + '@esbuild/openbsd-arm64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.21.5': + '@esbuild/openbsd-arm64@0.28.0': optional: true - '@esbuild/netbsd-x64@0.27.3': + '@esbuild/openbsd-x64@0.21.5': optional: true - '@esbuild/openbsd-arm64@0.27.3': + '@esbuild/openbsd-x64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.21.5': + '@esbuild/openbsd-x64@0.28.0': optional: true - '@esbuild/openbsd-x64@0.27.3': + '@esbuild/openharmony-arm64@0.27.7': optional: true - '@esbuild/openharmony-arm64@0.27.3': + '@esbuild/openharmony-arm64@0.28.0': optional: true '@esbuild/sunos-x64@0.21.5': optional: true - '@esbuild/sunos-x64@0.27.3': + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.28.0': optional: true '@esbuild/win32-arm64@0.21.5': optional: true - '@esbuild/win32-arm64@0.27.3': + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.28.0': optional: true '@esbuild/win32-ia32@0.21.5': optional: true - '@esbuild/win32-ia32@0.27.3': + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.28.0': optional: true '@esbuild/win32-x64@0.21.5': optional: true - '@esbuild/win32-x64@0.27.3': + '@esbuild/win32-x64@0.27.7': + optional: true + + '@esbuild/win32-x64@0.28.0': optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@10.4.1(jiti@2.7.0))': + dependencies: + eslint: 10.4.1(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': dependencies: eslint: 8.57.1 @@ -6289,9 +7029,25 @@ snapshots: '@eslint-community/regexpp@4.12.2': {} + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.6.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/eslintrc@2.1.4': dependencies: - ajv: 6.14.0 + ajv: 6.15.0 debug: 4.4.3 espree: 9.6.1 globals: 13.24.0 @@ -6305,13 +7061,20 @@ snapshots: '@eslint/js@8.57.1': {} + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + '@fastify/accept-negotiator@2.0.1': {} '@fastify/ajv-compiler@4.0.5': dependencies: - ajv: 8.18.0 - ajv-formats: 3.0.1(ajv@8.18.0) - fast-uri: 3.1.0 + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + fast-uri: 3.1.2 '@fastify/busboy@3.2.0': {} @@ -6331,7 +7094,7 @@ snapshots: '@fastify/fast-json-stringify-compiler@5.0.3': dependencies: - fast-json-stringify: 6.3.0 + fast-json-stringify: 6.4.0 '@fastify/forwarded@3.0.1': {} @@ -6363,7 +7126,13 @@ snapshots: '@fastify/proxy-addr@5.1.0': dependencies: '@fastify/forwarded': 3.0.1 - ipaddr.js: 2.3.0 + ipaddr.js: 2.4.0 + + '@fastify/rate-limit@10.3.0': + dependencies: + '@lukeed/ms': 2.0.2 + fastify-plugin: 5.1.0 + toad-cache: 3.7.1 '@fastify/send@4.1.0': dependencies: @@ -6382,23 +7151,23 @@ snapshots: fastq: 1.20.1 glob: 11.1.0 - '@gorhom/bottom-sheet@5.2.14(@types/react-native@0.70.19)(@types/react@19.2.14)(react-native-gesture-handler@2.31.2(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-reanimated@3.19.5(@babel/core@7.29.0)(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)': + '@gorhom/bottom-sheet@5.2.14(@types/react-native@0.70.19)(@types/react@19.2.15)(react-native-gesture-handler@2.31.2(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native-reanimated@3.19.5(@babel/core@7.29.7)(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3)': dependencies: - '@gorhom/portal': 1.0.14(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + '@gorhom/portal': 1.0.14(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) invariant: 2.2.4 react: 19.2.3 - react-native: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) - react-native-gesture-handler: 2.31.2(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) - react-native-reanimated: 3.19.5(@babel/core@7.29.0)(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) + react-native-gesture-handler: 2.31.2(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) + react-native-reanimated: 3.19.5(@babel/core@7.29.7)(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 '@types/react-native': 0.70.19 - '@gorhom/portal@1.0.14(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)': + '@gorhom/portal@1.0.14(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3)': dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 react: 19.2.3 - react-native: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) '@hapi/hoek@9.3.0': {} @@ -6406,6 +7175,18 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -6418,7 +7199,9 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} - '@ioredis/commands@1.5.1': {} + '@humanwhocodes/retry@0.4.3': {} + + '@ioredis/commands@1.10.0': {} '@isaacs/cliui@9.0.0': {} @@ -6432,12 +7215,12 @@ snapshots: js-yaml: 3.14.2 resolve-from: 5.0.0 - '@istanbuljs/schema@0.1.3': {} + '@istanbuljs/schema@0.1.6': {} '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 22.19.19 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -6450,14 +7233,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 22.19.19 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.19.15) + jest-config: 29.7.0(@types/node@22.19.19) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -6486,7 +7269,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 22.19.19 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -6504,7 +7287,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.19.15 + '@types/node': 22.19.19 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -6526,7 +7309,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 22.19.15 + '@types/node': 22.19.19 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit: 0.1.2 @@ -6573,7 +7356,7 @@ snapshots: '@jest/transform@29.7.0': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.7 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.31 babel-plugin-istanbul: 6.1.1 @@ -6596,7 +7379,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.19.15 + '@types/node': 22.19.19 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -6626,6 +7409,13 @@ snapshots: '@lukeed/ms@2.0.2': {} + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': dependencies: eslint-scope: 5.1.1 @@ -6642,49 +7432,51 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@package-json/types@0.0.12': {} + '@pinojs/redact@0.4.0': {} '@polka/url@1.0.0-next.29': {} - '@prisma/client@6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3)': + '@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3)': optionalDependencies: - prisma: 6.19.2(typescript@5.9.3) + prisma: 6.19.3(typescript@5.9.3) typescript: 5.9.3 - '@prisma/config@6.19.2': + '@prisma/config@6.19.3': dependencies: c12: 3.1.0 deepmerge-ts: 7.1.5 - effect: 3.18.4 + effect: 3.21.0 empathic: 2.0.0 transitivePeerDependencies: - magicast - '@prisma/debug@6.19.2': {} + '@prisma/debug@6.19.3': {} '@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7': {} - '@prisma/engines@6.19.2': + '@prisma/engines@6.19.3': dependencies: - '@prisma/debug': 6.19.2 + '@prisma/debug': 6.19.3 '@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7 - '@prisma/fetch-engine': 6.19.2 - '@prisma/get-platform': 6.19.2 + '@prisma/fetch-engine': 6.19.3 + '@prisma/get-platform': 6.19.3 - '@prisma/fetch-engine@6.19.2': + '@prisma/fetch-engine@6.19.3': dependencies: - '@prisma/debug': 6.19.2 + '@prisma/debug': 6.19.3 '@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7 - '@prisma/get-platform': 6.19.2 + '@prisma/get-platform': 6.19.3 - '@prisma/get-platform@6.19.2': + '@prisma/get-platform@6.19.3': dependencies: - '@prisma/debug': 6.19.2 + '@prisma/debug': 6.19.3 - '@react-native-async-storage/async-storage@2.2.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))': + '@react-native-async-storage/async-storage@2.2.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))': dependencies: merge-options: 3.0.4 - react-native: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) '@react-native-community/cli-clean@20.1.0': dependencies: @@ -6697,7 +7489,7 @@ snapshots: dependencies: '@react-native-community/cli-tools': 20.1.0 fast-glob: 3.3.3 - fast-xml-parser: 4.5.4 + fast-xml-parser: 4.5.6 picocolors: 1.1.1 '@react-native-community/cli-config-apple@20.1.0': @@ -6732,9 +7524,9 @@ snapshots: node-stream-zip: 1.15.0 ora: 5.4.1 picocolors: 1.1.1 - semver: 7.7.4 + semver: 7.8.1 wcwidth: 1.0.1 - yaml: 2.8.2 + yaml: 2.9.0 transitivePeerDependencies: - typescript @@ -6751,7 +7543,7 @@ snapshots: '@react-native-community/cli-config-apple': 20.1.0 '@react-native-community/cli-tools': 20.1.0 execa: 5.1.1 - fast-xml-parser: 4.5.4 + fast-xml-parser: 4.5.6 picocolors: 1.1.1 '@react-native-community/cli-platform-ios@20.1.0': @@ -6761,7 +7553,7 @@ snapshots: '@react-native-community/cli-server-api@20.1.0': dependencies: '@react-native-community/cli-tools': 20.1.0 - body-parser: 1.20.4 + body-parser: 1.20.5 compression: 1.8.1 connect: 3.7.0 errorhandler: 1.5.2 @@ -6769,7 +7561,7 @@ snapshots: open: 6.4.0 pretty-format: 29.7.0 serve-static: 1.16.3 - ws: 6.2.3 + ws: 6.2.4 transitivePeerDependencies: - bufferutil - supports-color @@ -6781,12 +7573,12 @@ snapshots: appdirsjs: 1.2.7 execa: 5.1.1 find-up: 5.0.0 - launch-editor: 2.13.1 + launch-editor: 2.14.0 mime: 2.6.0 ora: 5.4.1 picocolors: 1.1.1 prompts: 2.4.2 - semver: 7.7.4 + semver: 7.8.1 '@react-native-community/cli-types@20.1.0': dependencies: @@ -6808,7 +7600,7 @@ snapshots: graceful-fs: 4.2.11 picocolors: 1.1.1 prompts: 2.4.2 - semver: 7.7.4 + semver: 7.8.1 transitivePeerDependencies: - bufferutil - supports-color @@ -6817,74 +7609,74 @@ snapshots: '@react-native/assets-registry@0.84.1': {} - '@react-native/babel-plugin-codegen@0.84.1(@babel/core@7.29.0)': + '@react-native/babel-plugin-codegen@0.84.1(@babel/core@7.29.7)': dependencies: - '@babel/traverse': 7.29.0 - '@react-native/codegen': 0.84.1(@babel/core@7.29.0) + '@babel/traverse': 7.29.7 + '@react-native/codegen': 0.84.1(@babel/core@7.29.7) transitivePeerDependencies: - '@babel/core' - supports-color - '@react-native/babel-preset@0.84.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) - '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) - '@react-native/babel-plugin-codegen': 0.84.1(@babel/core@7.29.0) + '@react-native/babel-preset@0.84.1(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-proposal-export-default-from': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-export-default-from': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-flow-strip-types': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-display-name': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-regenerator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7) + '@react-native/babel-plugin-codegen': 0.84.1(@babel/core@7.29.7) babel-plugin-syntax-hermes-parser: 0.32.0 - babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0) + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.7) react-refresh: 0.14.2 transitivePeerDependencies: - supports-color - '@react-native/codegen@0.84.1(@babel/core@7.29.0)': + '@react-native/codegen@0.84.1(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 hermes-parser: 0.32.0 invariant: 2.2.4 nullthrows: 1.1.1 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 yargs: 17.7.2 - '@react-native/community-cli-plugin@0.84.1(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))': + '@react-native/community-cli-plugin@0.84.1(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))': dependencies: '@react-native/dev-middleware': 0.84.1 debug: 4.4.3 invariant: 2.2.4 - metro: 0.83.5 - metro-config: 0.83.5 - metro-core: 0.83.5 - semver: 7.7.4 + metro: 0.83.7 + metro-config: 0.83.7 + metro-core: 0.83.7 + semver: 7.8.1 optionalDependencies: '@react-native-community/cli': 20.1.0(typescript@5.9.3) - '@react-native/metro-config': 0.84.1(@babel/core@7.29.0) + '@react-native/metro-config': 0.84.1(@babel/core@7.29.7) transitivePeerDependencies: - bufferutil - supports-color @@ -6913,26 +7705,26 @@ snapshots: nullthrows: 1.1.1 open: 7.4.2 serve-static: 1.16.3 - ws: 7.5.10 + ws: 7.5.11 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@react-native/eslint-config@0.84.1(eslint@8.57.1)(jest@29.7.0(@types/node@22.19.15))(prettier@2.8.8)(typescript@5.9.3)': + '@react-native/eslint-config@0.84.1(eslint@8.57.1)(jest@29.7.0(@types/node@22.19.19))(prettier@2.8.8)(typescript@5.9.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/eslint-parser': 7.28.6(@babel/core@7.29.0)(eslint@8.57.1) + '@babel/core': 7.29.7 + '@babel/eslint-parser': 7.29.7(@babel/core@7.29.7)(eslint@8.57.1) '@react-native/eslint-plugin': 0.84.1 - '@typescript-eslint/eslint-plugin': 8.57.0(@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': 8.57.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.60.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-config-prettier: 8.10.2(eslint@8.57.1) eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.1) - eslint-plugin-ft-flow: 2.0.3(@babel/eslint-parser@7.28.6(@babel/core@7.29.0)(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-jest: 29.15.0(@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(jest@29.7.0(@types/node@22.19.15))(typescript@5.9.3) + eslint-plugin-ft-flow: 2.0.3(@babel/eslint-parser@7.29.7(@babel/core@7.29.7)(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-jest: 29.15.2(@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(jest@29.7.0(@types/node@22.19.19))(typescript@5.9.3) eslint-plugin-react: 7.37.5(eslint@8.57.1) - eslint-plugin-react-hooks: 7.0.1(eslint@8.57.1) + eslint-plugin-react-hooks: 7.1.1(eslint@8.57.1) eslint-plugin-react-native: 5.0.0(eslint@8.57.1) prettier: 2.8.8 transitivePeerDependencies: @@ -6946,33 +7738,33 @@ snapshots: '@react-native/js-polyfills@0.84.1': {} - '@react-native/metro-babel-transformer@0.84.1(@babel/core@7.29.0)': + '@react-native/metro-babel-transformer@0.84.1(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@react-native/babel-preset': 0.84.1(@babel/core@7.29.0) + '@babel/core': 7.29.7 + '@react-native/babel-preset': 0.84.1(@babel/core@7.29.7) hermes-parser: 0.32.0 nullthrows: 1.1.1 transitivePeerDependencies: - supports-color - '@react-native/metro-config@0.84.1(@babel/core@7.29.0)': + '@react-native/metro-config@0.84.1(@babel/core@7.29.7)': dependencies: '@react-native/js-polyfills': 0.84.1 - '@react-native/metro-babel-transformer': 0.84.1(@babel/core@7.29.0) - metro-config: 0.83.5 - metro-runtime: 0.83.5 + '@react-native/metro-babel-transformer': 0.84.1(@babel/core@7.29.7) + metro-config: 0.83.7 + metro-runtime: 0.83.7 transitivePeerDependencies: - '@babel/core' - bufferutil - supports-color - utf-8-validate - '@react-native/new-app-screen@0.84.1(@types/react@19.2.14)(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)': + '@react-native/new-app-screen@0.84.1(@types/react@19.2.15)(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3)': dependencies: react: 19.2.3 - react-native: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 '@react-native/normalize-colors@0.74.89': {} @@ -6980,151 +7772,151 @@ snapshots: '@react-native/typescript-config@0.84.1': {} - '@react-native/virtualized-lists@0.84.1(@types/react@19.2.14)(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)': + '@react-native/virtualized-lists@0.84.1(@types/react@19.2.15)(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 react: 19.2.3 - react-native: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@react-navigation/bottom-tabs@7.15.5(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)': + '@react-navigation/bottom-tabs@7.16.2(@react-navigation/native@7.2.5(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.8.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native-screens@4.25.2(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3)': dependencies: - '@react-navigation/elements': 2.9.10(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) - '@react-navigation/native': 7.1.33(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + '@react-navigation/elements': 2.9.19(@react-navigation/native@7.2.5(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.8.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) + '@react-navigation/native': 7.2.5(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) color: 4.2.3 react: 19.2.3 - react-native: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) - react-native-safe-area-context: 5.7.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) - react-native-screens: 4.24.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) + react-native-safe-area-context: 5.8.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) + react-native-screens: 4.25.2(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) sf-symbols-typescript: 2.2.0 transitivePeerDependencies: - '@react-native-masked-view/masked-view' - '@react-navigation/core@7.16.1(react@19.2.3)': + '@react-navigation/core@7.17.5(react@19.2.3)': dependencies: - '@react-navigation/routers': 7.5.3 + '@react-navigation/routers': 7.5.5 escape-string-regexp: 4.0.0 fast-deep-equal: 3.1.3 - nanoid: 3.3.11 + nanoid: 3.3.12 query-string: 7.1.3 react: 19.2.3 - react-is: 19.2.4 + react-is: 19.2.6 use-latest-callback: 0.2.6(react@19.2.3) use-sync-external-store: 1.6.0(react@19.2.3) - '@react-navigation/elements@2.9.10(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)': + '@react-navigation/elements@2.9.19(@react-navigation/native@7.2.5(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.8.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3)': dependencies: - '@react-navigation/native': 7.1.33(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + '@react-navigation/native': 7.2.5(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) color: 4.2.3 react: 19.2.3 - react-native: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) - react-native-safe-area-context: 5.7.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) + react-native-safe-area-context: 5.8.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) use-latest-callback: 0.2.6(react@19.2.3) use-sync-external-store: 1.6.0(react@19.2.3) - '@react-navigation/native-stack@7.14.4(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)': + '@react-navigation/native-stack@7.16.0(@react-navigation/native@7.2.5(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.8.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native-screens@4.25.2(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3)': dependencies: - '@react-navigation/elements': 2.9.10(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) - '@react-navigation/native': 7.1.33(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + '@react-navigation/elements': 2.9.19(@react-navigation/native@7.2.5(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.8.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) + '@react-navigation/native': 7.2.5(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) color: 4.2.3 react: 19.2.3 - react-native: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) - react-native-safe-area-context: 5.7.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) - react-native-screens: 4.24.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) + react-native-safe-area-context: 5.8.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) + react-native-screens: 4.25.2(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) sf-symbols-typescript: 2.2.0 warn-once: 0.1.1 transitivePeerDependencies: - '@react-native-masked-view/masked-view' - '@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)': + '@react-navigation/native@7.2.5(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3)': dependencies: - '@react-navigation/core': 7.16.1(react@19.2.3) + '@react-navigation/core': 7.17.5(react@19.2.3) escape-string-regexp: 4.0.0 fast-deep-equal: 3.1.3 - nanoid: 3.3.11 + nanoid: 3.3.12 react: 19.2.3 - react-native: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) use-latest-callback: 0.2.6(react@19.2.3) - '@react-navigation/routers@7.5.3': + '@react-navigation/routers@7.5.5': dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 - '@rollup/rollup-android-arm-eabi@4.59.0': + '@rollup/rollup-android-arm-eabi@4.60.4': optional: true - '@rollup/rollup-android-arm64@4.59.0': + '@rollup/rollup-android-arm64@4.60.4': optional: true - '@rollup/rollup-darwin-arm64@4.59.0': + '@rollup/rollup-darwin-arm64@4.60.4': optional: true - '@rollup/rollup-darwin-x64@4.59.0': + '@rollup/rollup-darwin-x64@4.60.4': optional: true - '@rollup/rollup-freebsd-arm64@4.59.0': + '@rollup/rollup-freebsd-arm64@4.60.4': optional: true - '@rollup/rollup-freebsd-x64@4.59.0': + '@rollup/rollup-freebsd-x64@4.60.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.59.0': + '@rollup/rollup-linux-arm-musleabihf@4.60.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.59.0': + '@rollup/rollup-linux-arm64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.59.0': + '@rollup/rollup-linux-arm64-musl@4.60.4': optional: true - '@rollup/rollup-linux-loong64-gnu@4.59.0': + '@rollup/rollup-linux-loong64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-loong64-musl@4.59.0': + '@rollup/rollup-linux-loong64-musl@4.60.4': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.59.0': + '@rollup/rollup-linux-ppc64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-ppc64-musl@4.59.0': + '@rollup/rollup-linux-ppc64-musl@4.60.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.59.0': + '@rollup/rollup-linux-riscv64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-riscv64-musl@4.59.0': + '@rollup/rollup-linux-riscv64-musl@4.60.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.59.0': + '@rollup/rollup-linux-s390x-gnu@4.60.4': optional: true - '@rollup/rollup-linux-x64-gnu@4.59.0': + '@rollup/rollup-linux-x64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-x64-musl@4.59.0': + '@rollup/rollup-linux-x64-musl@4.60.4': optional: true - '@rollup/rollup-openbsd-x64@4.59.0': + '@rollup/rollup-openbsd-x64@4.60.4': optional: true - '@rollup/rollup-openharmony-arm64@4.59.0': + '@rollup/rollup-openharmony-arm64@4.60.4': optional: true - '@rollup/rollup-win32-arm64-msvc@4.59.0': + '@rollup/rollup-win32-arm64-msvc@4.60.4': optional: true - '@rollup/rollup-win32-ia32-msvc@4.59.0': + '@rollup/rollup-win32-ia32-msvc@4.60.4': optional: true - '@rollup/rollup-win32-x64-gnu@4.59.0': + '@rollup/rollup-win32-x64-gnu@4.60.4': optional: true - '@rollup/rollup-win32-x64-msvc@4.59.0': + '@rollup/rollup-win32-x64-msvc@4.60.4': optional: true '@sideway/address@4.1.5': @@ -7147,79 +7939,88 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': + '@sveltejs/acorn-typescript@1.0.10(acorn@8.16.0)': dependencies: acorn: 8.16.0 - '@sveltejs/adapter-auto@7.0.1(@sveltejs/kit@2.54.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.10)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.10)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))': + '@sveltejs/adapter-auto@7.0.1(@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.56.0(@typescript-eslint/types@8.60.0))(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.56.0(@typescript-eslint/types@8.60.0))(typescript@5.9.3)(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)))': dependencies: - '@sveltejs/kit': 2.54.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.10)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.10)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/kit': 2.61.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.56.0(@typescript-eslint/types@8.60.0))(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.56.0(@typescript-eslint/types@8.60.0))(typescript@5.9.3)(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)) - '@sveltejs/kit@2.54.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.10)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.10)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.56.0(@typescript-eslint/types@8.60.0))(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.56.0(@typescript-eslint/types@8.60.0))(typescript@5.9.3)(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0))': dependencies: '@standard-schema/spec': 1.1.0 - '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.10)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/acorn-typescript': 1.0.10(acorn@8.16.0) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.56.0(@typescript-eslint/types@8.60.0))(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)) '@types/cookie': 0.6.0 acorn: 8.16.0 cookie: 0.6.0 - devalue: 5.6.4 + devalue: 5.8.1 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 mrmime: 2.0.1 - set-cookie-parser: 3.0.1 + set-cookie-parser: 3.1.0 sirv: 3.0.2 - svelte: 5.53.10 - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + svelte: 5.56.0(@typescript-eslint/types@8.60.0) + vite: 7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0) optionalDependencies: typescript: 5.9.3 - '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.10)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.10)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.56.0(@typescript-eslint/types@8.60.0))(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.56.0(@typescript-eslint/types@8.60.0))(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.10)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.56.0(@typescript-eslint/types@8.60.0))(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)) obug: 2.1.1 - svelte: 5.53.10 - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + svelte: 5.56.0(@typescript-eslint/types@8.60.0) + vite: 7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0) - '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.10)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.56.0(@typescript-eslint/types@8.60.0))(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.10)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.10)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.56.0(@typescript-eslint/types@8.60.0))(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.56.0(@typescript-eslint/types@8.60.0))(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)) deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 - svelte: 5.53.10 - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - vitefu: 1.1.2(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + svelte: 5.56.0(@typescript-eslint/types@8.60.0) + vite: 7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0) + vitefu: 1.1.3(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)) + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@types/cookie@0.6.0': {} + '@types/esrecurse@4.3.1': {} + '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} + '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.19.15 + '@types/node': 22.19.19 '@types/hammerjs@2.0.46': {} @@ -7238,28 +8039,30 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 - '@types/node@22.19.15': + '@types/json-schema@7.0.15': {} + + '@types/node@22.19.19': dependencies: undici-types: 6.21.0 '@types/qrcode@1.5.6': dependencies: - '@types/node': 22.19.15 + '@types/node': 22.19.19 '@types/react-native-vector-icons@6.4.18': dependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 '@types/react-native': 0.70.19 '@types/react-native@0.70.19': dependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 '@types/react-test-renderer@19.1.0': dependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@types/react@19.2.14': + '@types/react@19.2.15': dependencies: csstype: 3.2.3 @@ -7273,98 +8076,219 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.0 - '@typescript-eslint/type-utils': 8.57.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.0 + '@typescript-eslint/parser': 8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/type-utils': 8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.60.0 + eslint: 10.4.1(jiti@2.7.0) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.60.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/type-utils': 8.60.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.60.0 eslint: 8.57.1 ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/parser@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.57.0 - '@typescript-eslint/types': 8.57.0 - '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.0 + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.60.0 + debug: 4.4.3 + eslint: 10.4.1(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.60.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.60.0 debug: 4.4.3 eslint: 8.57.1 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.57.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.60.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3) - '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.9.3) + '@typescript-eslint/types': 8.60.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.57.0': + '@typescript-eslint/scope-manager@8.60.0': + dependencies: + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/visitor-keys': 8.60.0 + + '@typescript-eslint/tsconfig-utils@8.60.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.57.0 - '@typescript-eslint/visitor-keys': 8.57.0 + typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.57.0(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)': dependencies: + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + debug: 4.4.3 + eslint: 10.4.1(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 + transitivePeerDependencies: + - supports-color - '@typescript-eslint/type-utils@8.57.0(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.60.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.57.0 - '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.0(eslint@8.57.1)(typescript@5.9.3) debug: 4.4.3 eslint: 8.57.1 - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.57.0': {} + '@typescript-eslint/types@8.60.0': {} - '@typescript-eslint/typescript-estree@8.57.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.60.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.57.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3) - '@typescript-eslint/types': 8.57.0 - '@typescript-eslint/visitor-keys': 8.57.0 + '@typescript-eslint/project-service': 8.60.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.9.3) + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/visitor-keys': 8.60.0 debug: 4.4.3 - minimatch: 10.2.4 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) + minimatch: 10.2.5 + semver: 7.8.1 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) + eslint: 10.4.1(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.57.0(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/utils@8.60.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) - '@typescript-eslint/scope-manager': 8.57.0 - '@typescript-eslint/types': 8.57.0 - '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) eslint: 8.57.1 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.57.0': + '@typescript-eslint/visitor-keys@8.60.0': dependencies: - '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/types': 8.60.0 eslint-visitor-keys: 5.0.1 - '@ungap/structured-clone@1.3.0': {} + '@ungap/structured-clone@1.3.1': {} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + optional: true '@vitest/expect@2.1.9': dependencies: @@ -7373,13 +8297,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.15)(terser@5.46.0))': + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.19)(terser@5.48.0))': dependencies: '@vitest/spy': 2.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 5.4.21(@types/node@22.19.15)(terser@5.46.0) + vite: 5.4.21(@types/node@22.19.19)(terser@5.48.0) '@vitest/pretty-format@2.1.9': dependencies: @@ -7432,21 +8356,21 @@ snapshots: agent-base@7.1.4: {} - ajv-formats@3.0.1(ajv@8.18.0): + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: - ajv: 8.18.0 + ajv: 8.20.0 - ajv@6.14.0: + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.18.0: + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 + fast-uri: 3.1.2 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -7479,7 +8403,7 @@ snapshots: anymatch@3.1.3: dependencies: normalize-path: 3.0.0 - picomatch: 2.3.1 + picomatch: 2.3.2 appdirsjs@1.2.7: {} @@ -7498,52 +8422,52 @@ snapshots: array-includes@3.1.9: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.1 - es-object-atoms: 1.1.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 get-intrinsic: 1.3.0 is-string: 1.1.1 math-intrinsics: 1.1.0 array.prototype.findlast@1.2.5: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 es-shim-unscopables: 1.1.0 array.prototype.flat@1.3.3: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-shim-unscopables: 1.1.0 array.prototype.flatmap@1.3.3: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-shim-unscopables: 1.1.0 array.prototype.tosorted@1.1.4: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-errors: 1.3.0 es-shim-unscopables: 1.1.0 arraybuffer.prototype.slice@1.0.4: dependencies: array-buffer-byte-length: 1.0.2 - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-errors: 1.3.0 get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 @@ -7578,13 +8502,13 @@ snapshots: axobject-query@4.1.0: {} - babel-jest@29.7.0(@babel/core@7.29.0): + babel-jest@29.7.0(@babel/core@7.29.7): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.7 '@jest/transform': 29.7.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.29.0) + babel-preset-jest: 29.6.3(@babel/core@7.29.7) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -7593,9 +8517,9 @@ snapshots: babel-plugin-istanbul@6.1.1: dependencies: - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.3 + '@istanbuljs/schema': 0.1.6 istanbul-lib-instrument: 5.2.1 test-exclude: 6.0.0 transitivePeerDependencies: @@ -7603,40 +8527,40 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.28.0 - babel-plugin-polyfill-corejs2@0.4.16(@babel/core@7.29.0): + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.7): dependencies: - '@babel/compat-data': 7.29.0 - '@babel/core': 7.29.0 - '@babel/helper-define-polyfill-provider': 0.6.7(@babel/core@7.29.0) + '@babel/compat-data': 7.29.7 + '@babel/core': 7.29.7 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0): + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.7): dependencies: - '@babel/core': 7.29.0 - '@babel/helper-define-polyfill-provider': 0.6.7(@babel/core@7.29.0) - core-js-compat: 3.48.0 + '@babel/core': 7.29.7 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + core-js-compat: 3.49.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.14.1(@babel/core@7.29.0): + babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.7): dependencies: - '@babel/core': 7.29.0 - '@babel/helper-define-polyfill-provider': 0.6.7(@babel/core@7.29.0) - core-js-compat: 3.48.0 + '@babel/core': 7.29.7 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + core-js-compat: 3.49.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.7(@babel/core@7.29.0): + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.7): dependencies: - '@babel/core': 7.29.0 - '@babel/helper-define-polyfill-provider': 0.6.7(@babel/core@7.29.0) + '@babel/core': 7.29.7 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) transitivePeerDependencies: - supports-color @@ -7644,44 +8568,46 @@ snapshots: dependencies: hermes-parser: 0.32.0 - babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.29.0): + babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.29.7): dependencies: - '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-flow': 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - '@babel/core' - babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) - '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) - - babel-preset-jest@29.6.3(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.7) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.7) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.7) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.7) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.7) + + babel-preset-jest@29.6.3(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) balanced-match@1.0.2: {} balanced-match@4.0.4: {} + base64-arraybuffer@1.0.2: {} + base64-js@1.5.1: {} - baseline-browser-mapping@2.10.0: {} + baseline-browser-mapping@2.10.33: {} bl@4.1.0: dependencies: @@ -7691,7 +8617,7 @@ snapshots: bn.js@4.12.3: {} - body-parser@1.20.4: + body-parser@1.20.5: dependencies: bytes: 3.1.2 content-type: 1.0.5 @@ -7701,7 +8627,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.14.2 + qs: 6.15.2 raw-body: 2.5.3 type-is: 1.6.18 unpipe: 1.0.0 @@ -7710,12 +8636,12 @@ snapshots: boolbase@1.0.0: {} - brace-expansion@1.1.12: + brace-expansion@1.1.15: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.4: + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -7723,13 +8649,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.1: + browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.0 - caniuse-lite: 1.0.30001778 - electron-to-chromium: 1.5.313 - node-releases: 2.0.36 - update-browserslist-db: 1.2.3(browserslist@4.28.1) + baseline-browser-mapping: 2.10.33 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.364 + node-releases: 2.0.46 + update-browserslist-db: 1.2.3(browserslist@4.28.2) bser@2.1.1: dependencies: @@ -7742,21 +8668,23 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + builtin-modules@5.2.0: {} + bytes@3.1.2: {} c12@3.1.0: dependencies: chokidar: 4.0.3 confbox: 0.2.4 - defu: 6.1.4 + defu: 6.1.7 dotenv: 16.6.1 exsolve: 1.0.8 giget: 2.0.0 - jiti: 2.6.1 + jiti: 2.7.0 ohash: 2.0.11 pathe: 2.0.3 perfect-debounce: 1.0.0 - pkg-types: 2.3.0 + pkg-types: 2.3.1 rc9: 2.1.2 cac@6.7.14: {} @@ -7766,7 +8694,7 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - call-bind@1.0.8: + call-bind@1.0.9: dependencies: call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 @@ -7784,7 +8712,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001778: {} + caniuse-lite@1.0.30001793: {} chai@5.3.3: dependencies: @@ -7799,6 +8727,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + change-case@5.4.4: {} + char-regex@1.0.2: {} check-error@2.1.3: {} @@ -7809,7 +8739,7 @@ snapshots: chrome-launcher@0.15.2: dependencies: - '@types/node': 22.19.15 + '@types/node': 22.19.19 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2 @@ -7818,7 +8748,7 @@ snapshots: chromium-edge-launcher@0.2.0: dependencies: - '@types/node': 22.19.15 + '@types/node': 22.19.19 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2 @@ -7831,14 +8761,20 @@ snapshots: ci-info@3.9.0: {} + ci-info@4.4.0: {} + citty@0.1.6: dependencies: consola: 3.4.2 - citty@0.2.1: {} + citty@0.2.2: {} cjs-module-lexer@1.4.3: {} + clean-regexp@1.0.0: + dependencies: + escape-string-regexp: 1.0.5 + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -7867,7 +8803,7 @@ snapshots: clsx@2.1.1: {} - cluster-key-slot@1.1.2: {} + cluster-key-slot@1.1.1: {} co@4.6.0: {} @@ -7907,6 +8843,8 @@ snapshots: commander@9.5.0: {} + comment-parser@1.4.7: {} + compressible@2.0.18: dependencies: mime-db: 1.54.0 @@ -7959,9 +8897,9 @@ snapshots: cookie@1.1.1: {} - core-js-compat@3.48.0: + core-js-compat@3.49.0: dependencies: - browserslist: 4.28.1 + browserslist: 4.28.2 cosmiconfig@9.0.1(typescript@5.9.3): dependencies: @@ -7972,13 +8910,13 @@ snapshots: optionalDependencies: typescript: 5.9.3 - create-jest@29.7.0(@types/node@22.19.15): + create-jest@29.7.0(@types/node@22.19.19): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.19.15) + jest-config: 29.7.0(@types/node@22.19.19) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -8003,6 +8941,10 @@ snapshots: dependencies: hyphenate-style-name: 1.1.0 + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + css-select@5.2.2: dependencies: boolbase: 1.0.0 @@ -8040,7 +8982,7 @@ snapshots: dateformat@4.6.3: {} - dayjs@1.11.20: {} + dayjs@1.11.21: {} debug@2.6.9: dependencies: @@ -8080,7 +9022,7 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - defu@6.1.4: {} + defu@6.1.7: {} denque@2.1.0: {} @@ -8094,7 +9036,7 @@ snapshots: detect-newline@3.1.0: {} - devalue@5.6.4: {} + devalue@5.8.1: {} diff-sequences@29.6.3: {} @@ -8140,12 +9082,12 @@ snapshots: ee-first@1.1.1: {} - effect@3.18.4: + effect@3.21.0: dependencies: '@standard-schema/spec': 1.1.0 fast-check: 3.23.2 - electron-to-chromium@1.5.313: {} + electron-to-chromium@1.5.364: {} emittery@0.13.1: {} @@ -8161,6 +9103,11 @@ snapshots: dependencies: once: 1.4.0 + enhanced-resolve@5.22.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + entities@4.5.0: {} env-paths@2.2.1: {} @@ -8180,19 +9127,19 @@ snapshots: accepts: 1.3.8 escape-html: 1.0.3 - es-abstract@1.24.1: + es-abstract@1.24.2: 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-bind: 1.0.9 call-bound: 1.0.4 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.1.1 + es-object-atoms: 1.1.2 es-set-tostringtag: 2.1.0 es-to-primitive: 1.3.0 function.prototype.name: 1.1.8 @@ -8204,7 +9151,7 @@ snapshots: has-property-descriptors: 1.0.2 has-proto: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.4 internal-slot: 1.1.0 is-array-buffer: 3.0.5 is-callable: 1.2.7 @@ -8222,7 +9169,7 @@ snapshots: object.assign: 4.1.7 own-keys: 1.0.1 regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.3 + safe-array-concat: 1.1.4 safe-push-apply: 1.0.0 safe-regex-test: 1.1.0 set-proto: 1.0.0 @@ -8233,20 +9180,20 @@ snapshots: 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 + typed-array-length: 1.0.8 unbox-primitive: 1.1.0 - which-typed-array: 1.1.20 + which-typed-array: 1.1.21 es-define-property@1.0.1: {} es-errors@1.3.0: {} - es-iterator-helpers@1.3.0: + es-iterator-helpers@1.3.2: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-errors: 1.3.0 es-set-tostringtag: 2.1.0 function-bind: 1.1.2 @@ -8259,11 +9206,10 @@ snapshots: internal-slot: 1.1.0 iterator.prototype: 1.1.5 math-intrinsics: 1.1.0 - safe-array-concat: 1.1.3 es-module-lexer@1.7.0: {} - es-object-atoms@1.1.1: + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -8272,11 +9218,11 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 - hasown: 2.0.2 + hasown: 2.0.4 es-shim-unscopables@1.1.0: dependencies: - hasown: 2.0.2 + hasown: 2.0.4 es-to-primitive@1.3.0: dependencies: @@ -8310,34 +9256,63 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 - esbuild@0.27.3: + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + esbuild@0.28.0: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 escalade@3.2.0: {} @@ -8349,38 +9324,109 @@ snapshots: escape-string-regexp@4.0.0: {} + eslint-compat-utils@0.5.1(eslint@10.4.1(jiti@2.7.0)): + dependencies: + eslint: 10.4.1(jiti@2.7.0) + semver: 7.8.1 + eslint-config-prettier@8.10.2(eslint@8.57.1): dependencies: eslint: 8.57.1 + eslint-import-context@0.1.9(unrs-resolver@1.12.2): + dependencies: + get-tsconfig: 4.14.0 + stable-hash-x: 0.2.0 + optionalDependencies: + unrs-resolver: 1.12.2 + + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0)))(eslint@10.4.1(jiti@2.7.0)): + dependencies: + debug: 4.4.3 + eslint: 10.4.1(jiti@2.7.0) + eslint-import-context: 0.1.9(unrs-resolver@1.12.2) + get-tsconfig: 4.14.0 + is-bun-module: 2.0.0 + stable-hash-x: 0.2.0 + tinyglobby: 0.2.16 + unrs-resolver: 1.12.2 + optionalDependencies: + eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-es-x@7.8.0(eslint@10.4.1(jiti@2.7.0)): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + eslint: 10.4.1(jiti@2.7.0) + eslint-compat-utils: 0.5.1(eslint@10.4.1(jiti@2.7.0)) + eslint-plugin-eslint-comments@3.2.0(eslint@8.57.1): dependencies: escape-string-regexp: 1.0.5 eslint: 8.57.1 ignore: 5.3.2 - eslint-plugin-ft-flow@2.0.3(@babel/eslint-parser@7.28.6(@babel/core@7.29.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-ft-flow@2.0.3(@babel/eslint-parser@7.29.7(@babel/core@7.29.7)(eslint@8.57.1))(eslint@8.57.1): dependencies: - '@babel/eslint-parser': 7.28.6(@babel/core@7.29.0)(eslint@8.57.1) + '@babel/eslint-parser': 7.29.7(@babel/core@7.29.7)(eslint@8.57.1) eslint: 8.57.1 - lodash: 4.17.23 + lodash: 4.18.1 string-natural-compare: 3.0.1 - eslint-plugin-jest@29.15.0(@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(jest@29.7.0(@types/node@22.19.15))(typescript@5.9.3): + eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0)): + dependencies: + '@package-json/types': 0.0.12 + '@typescript-eslint/types': 8.60.0 + comment-parser: 1.4.7 + debug: 4.4.3 + eslint: 10.4.1(jiti@2.7.0) + eslint-import-context: 0.1.9(unrs-resolver@1.12.2) + is-glob: 4.0.3 + minimatch: 10.2.5 + semver: 7.8.1 + stable-hash-x: 0.2.0 + unrs-resolver: 1.12.2 + optionalDependencies: + '@typescript-eslint/utils': 8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-jest@29.15.2(@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(jest@29.7.0(@types/node@22.19.19))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.57.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.57.0(@typescript-eslint/parser@8.57.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - jest: 29.7.0(@types/node@22.19.15) + '@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + jest: 29.7.0(@types/node@22.19.19) typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks@7.0.1(eslint@8.57.1): + eslint-plugin-n@18.0.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + enhanced-resolve: 5.22.1 + eslint: 10.4.1(jiti@2.7.0) + eslint-plugin-es-x: 7.8.0(eslint@10.4.1(jiti@2.7.0)) + get-tsconfig: 4.14.0 + globals: 15.15.0 + globrex: 0.1.2 + ignore: 5.3.2 + semver: 7.8.1 + optionalDependencies: + typescript: 5.9.3 + + eslint-plugin-promise@7.3.0(eslint@10.4.1(jiti@2.7.0)): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + eslint: 10.4.1(jiti@2.7.0) + + eslint-plugin-react-hooks@7.1.1(eslint@8.57.1): dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 eslint: 8.57.1 hermes-parser: 0.25.1 zod: 3.25.76 @@ -8402,21 +9448,45 @@ snapshots: array.prototype.flatmap: 1.3.3 array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 - es-iterator-helpers: 1.3.0 + es-iterator-helpers: 1.3.2 eslint: 8.57.1 estraverse: 5.3.0 - hasown: 2.0.2 + hasown: 2.0.4 jsx-ast-utils: 3.3.5 minimatch: 3.1.5 object.entries: 1.1.9 object.fromentries: 2.0.8 object.values: 1.2.1 prop-types: 15.8.1 - resolve: 2.0.0-next.6 + resolve: 2.0.0-next.7 semver: 6.3.1 string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 + eslint-plugin-security@4.0.0: + dependencies: + safe-regex: 2.1.1 + + eslint-plugin-unicorn@64.0.0(eslint@10.4.1(jiti@2.7.0)): + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + change-case: 5.4.4 + ci-info: 4.4.0 + clean-regexp: 1.0.0 + core-js-compat: 3.49.0 + eslint: 10.4.1(jiti@2.7.0) + find-up-simple: 1.0.1 + globals: 17.6.0 + indent-string: 5.0.0 + is-builtin-module: 5.0.0 + jsesc: 3.1.0 + pluralize: 8.0.0 + regexp-tree: 0.1.27 + regjsparser: 0.13.1 + semver: 7.8.1 + strip-indent: 4.1.1 + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -8427,12 +9497,56 @@ snapshots: esrecurse: 4.3.0 estraverse: 5.3.0 + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + eslint-visitor-keys@2.1.0: {} eslint-visitor-keys@3.4.3: {} eslint-visitor-keys@5.0.1: {} + eslint@10.4.1(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + eslint@8.57.1: dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) @@ -8442,8 +9556,8 @@ snapshots: '@humanwhocodes/config-array': 0.13.0 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.3.0 - ajv: 6.14.0 + '@ungap/structured-clone': 1.3.1 + ajv: 6.15.0 chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 @@ -8478,6 +9592,12 @@ snapshots: esm-env@1.2.2: {} + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + espree@9.6.1: dependencies: acorn: 8.16.0 @@ -8490,9 +9610,11 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.2.3: + esrap@2.2.9(@typescript-eslint/types@8.60.0): dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + optionalDependencies: + '@typescript-eslint/types': 8.60.0 esrecurse@4.3.0: dependencies: @@ -8504,7 +9626,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 esutils@2.0.3: {} @@ -8544,7 +9666,7 @@ snapshots: dependencies: pure-rand: 6.1.0 - fast-copy@4.0.2: {} + fast-copy@4.0.3: {} fast-decode-uri-component@1.0.1: {} @@ -8560,12 +9682,12 @@ snapshots: fast-json-stable-stringify@2.1.0: {} - fast-json-stringify@6.3.0: + fast-json-stringify@6.4.0: dependencies: '@fastify/merge-json-schemas': 0.2.1 - ajv: 8.18.0 - ajv-formats: 3.0.1(ajv@8.18.0) - fast-uri: 3.1.0 + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + fast-uri: 3.1.2 json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 @@ -8574,7 +9696,7 @@ snapshots: '@lukeed/ms': 2.0.2 asn1.js: 5.4.1 ecdsa-sig-formatter: 1.0.11 - mnemonist: 0.40.3 + mnemonist: 0.40.4 fast-levenshtein@2.0.6: {} @@ -8584,9 +9706,9 @@ snapshots: fast-safe-stringify@2.1.1: {} - fast-uri@3.1.0: {} + fast-uri@3.1.2: {} - fast-xml-parser@4.5.4: + fast-xml-parser@4.5.6: dependencies: strnum: 1.1.2 @@ -8596,7 +9718,7 @@ snapshots: fastify-plugin@5.1.0: {} - fastify@5.8.2: + fastify@5.8.5: dependencies: '@fastify/ajv-compiler': 4.0.5 '@fastify/error': 4.2.0 @@ -8604,15 +9726,15 @@ snapshots: '@fastify/proxy-addr': 5.1.0 abstract-logging: 2.0.1 avvio: 9.2.0 - fast-json-stringify: 6.3.0 - find-my-way: 9.5.0 + fast-json-stringify: 6.4.0 + find-my-way: 9.6.0 light-my-request: 6.6.0 pino: 10.3.1 process-warning: 5.0.0 rfdc: 1.4.1 secure-json-parse: 4.1.0 - semver: 7.7.4 - toad-cache: 3.7.0 + semver: 7.8.1 + toad-cache: 3.7.1 fastparallel@2.4.1: dependencies: @@ -8648,14 +9770,18 @@ snapshots: transitivePeerDependencies: - encoding - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -8674,11 +9800,13 @@ snapshots: transitivePeerDependencies: - supports-color - find-my-way@9.5.0: + find-my-way@9.6.0: dependencies: fast-deep-equal: 3.1.3 fast-querystring: 1.1.2 - safe-regex2: 5.0.0 + safe-regex2: 5.1.1 + + find-up-simple@1.0.1: {} find-up@4.1.0: dependencies: @@ -8692,11 +9820,16 @@ snapshots: flat-cache@3.2.0: dependencies: - flatted: 3.4.1 + flatted: 3.4.2 keyv: 4.5.4 rimraf: 3.0.2 - flatted@3.4.1: {} + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} flow-enums-runtime@0.0.6: {} @@ -8726,11 +9859,11 @@ snapshots: function.prototype.name@1.1.8: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 functions-have-names: 1.2.3 - hasown: 2.0.2 + hasown: 2.0.4 is-callable: 1.2.7 functions-have-names@1.2.3: {} @@ -8746,12 +9879,12 @@ snapshots: call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 function-bind: 1.1.2 get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.4 math-intrinsics: 1.1.0 get-package-type@0.1.0: {} @@ -8759,7 +9892,7 @@ snapshots: get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-stream@6.0.1: {} @@ -8769,7 +9902,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 - get-tsconfig@4.13.6: + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -8777,9 +9910,9 @@ snapshots: dependencies: citty: 0.1.6 consola: 3.4.2 - defu: 6.1.4 + defu: 6.1.7 node-fetch-native: 1.6.7 - nypm: 0.6.5 + nypm: 0.6.6 pathe: 2.0.3 glob-parent@5.1.2: @@ -8794,7 +9927,7 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 4.2.3 - minimatch: 10.2.4 + minimatch: 10.2.5 minipass: 7.1.3 package-json-from-dist: 1.0.1 path-scurry: 2.0.2 @@ -8812,11 +9945,17 @@ snapshots: dependencies: type-fest: 0.20.2 + globals@15.15.0: {} + + globals@17.6.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 gopd: 1.2.0 + globrex@0.1.2: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -8841,7 +9980,7 @@ snapshots: dependencies: has-symbols: 1.1.0 - hasown@2.0.2: + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -8855,7 +9994,7 @@ snapshots: hermes-estree@0.32.0: {} - hermes-estree@0.33.3: {} + hermes-estree@0.35.0: {} hermes-parser@0.25.1: dependencies: @@ -8865,9 +10004,9 @@ snapshots: dependencies: hermes-estree: 0.32.0 - hermes-parser@0.33.3: + hermes-parser@0.35.0: dependencies: - hermes-estree: 0.33.3 + hermes-estree: 0.35.0 hoist-non-react-statics@3.3.2: dependencies: @@ -8875,6 +10014,11 @@ snapshots: html-escaper@2.0.2: {} + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -8920,6 +10064,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@5.0.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -8934,32 +10080,30 @@ snapshots: internal-slot@1.1.0: dependencies: es-errors: 1.3.0 - hasown: 2.0.2 + hasown: 2.0.4 side-channel: 1.1.0 invariant@2.2.4: dependencies: loose-envify: 1.4.0 - ioredis@5.10.0: + ioredis@5.11.0: dependencies: - '@ioredis/commands': 1.5.1 - cluster-key-slot: 1.1.2 + '@ioredis/commands': 1.10.0 + cluster-key-slot: 1.1.1 debug: 4.4.3 denque: 2.1.0 - lodash.defaults: 4.2.0 - lodash.isarguments: 3.1.0 redis-errors: 1.2.0 redis-parser: 3.0.0 standard-as-callback: 2.1.0 transitivePeerDependencies: - supports-color - ipaddr.js@2.3.0: {} + ipaddr.js@2.4.0: {} is-array-buffer@3.0.5: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 get-intrinsic: 1.3.0 @@ -8984,11 +10128,19 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-builtin-module@5.0.0: + dependencies: + builtin-modules: 5.2.0 + + is-bun-module@2.0.0: + dependencies: + semver: 7.8.1 + is-callable@1.2.7: {} - is-core-module@2.16.1: + is-core-module@2.16.2: dependencies: - hasown: 2.0.2 + hasown: 2.0.4 is-data-view@1.0.2: dependencies: @@ -9046,14 +10198,14 @@ snapshots: is-reference@3.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 is-regex@1.2.1: dependencies: call-bound: 1.0.4 gopd: 1.2.0 has-tostringtag: 1.0.2 - hasown: 2.0.2 + hasown: 2.0.4 is-set@2.0.3: {} @@ -9076,7 +10228,7 @@ snapshots: is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.20 + which-typed-array: 1.1.21 is-unicode-supported@0.1.0: {} @@ -9105,9 +10257,9 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 - '@istanbuljs/schema': 0.1.3 + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + '@istanbuljs/schema': 0.1.6 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 transitivePeerDependencies: @@ -9115,11 +10267,11 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 - '@istanbuljs/schema': 0.1.3 + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + '@istanbuljs/schema': 0.1.6 istanbul-lib-coverage: 3.2.2 - semver: 7.7.4 + semver: 7.8.1 transitivePeerDependencies: - supports-color @@ -9145,7 +10297,7 @@ snapshots: iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-intrinsic: 1.3.0 get-proto: 1.0.1 has-symbols: 1.1.0 @@ -9167,7 +10319,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 22.19.19 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.2 @@ -9187,16 +10339,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.19.15): + jest-cli@29.7.0(@types/node@22.19.19): dependencies: '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.19.15) + create-jest: 29.7.0(@types/node@22.19.19) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.19.15) + jest-config: 29.7.0(@types/node@22.19.19) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -9206,12 +10358,12 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.19.15): + jest-config@29.7.0(@types/node@22.19.19): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.7 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.0) + babel-jest: 29.7.0(@babel/core@7.29.7) chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 @@ -9231,7 +10383,7 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.19.15 + '@types/node': 22.19.19 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -9260,7 +10412,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 22.19.19 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -9270,7 +10422,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.19.15 + '@types/node': 22.19.19 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -9296,7 +10448,7 @@ snapshots: jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 @@ -9309,7 +10461,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 22.19.19 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -9333,7 +10485,7 @@ snapshots: jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) jest-util: 29.7.0 jest-validate: 29.7.0 - resolve: 1.22.11 + resolve: 1.22.12 resolve.exports: 2.0.3 slash: 3.0.0 @@ -9344,7 +10496,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 22.19.19 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -9372,7 +10524,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 22.19.19 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.3 @@ -9392,15 +10544,15 @@ snapshots: jest-snapshot@29.7.0: dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) - '@babel/types': 7.29.0 + '@babel/core': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/types': 7.29.7 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) chalk: 4.1.2 expect: 29.7.0 graceful-fs: 4.2.11 @@ -9411,18 +10563,18 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.7.4 + semver: 7.8.1 transitivePeerDependencies: - supports-color jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 22.19.19 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 - picomatch: 2.3.1 + picomatch: 2.3.2 jest-validate@29.7.0: dependencies: @@ -9437,7 +10589,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.15 + '@types/node': 22.19.19 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -9446,24 +10598,24 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 22.19.15 + '@types/node': 22.19.19 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.19.15): + jest@29.7.0(@types/node@22.19.19): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.19.15) + jest-cli: 29.7.0(@types/node@22.19.19) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node - jiti@2.6.1: {} + jiti@2.7.0: {} joi@17.13.3: dependencies: @@ -9525,10 +10677,10 @@ snapshots: kleur@4.1.5: {} - launch-editor@2.13.1: + launch-editor@2.14.0: dependencies: picocolors: 1.1.1 - shell-quote: 1.8.3 + shell-quote: 1.8.4 leven@3.1.0: {} @@ -9564,15 +10716,11 @@ snapshots: lodash.debounce@4.0.8: {} - lodash.defaults@4.2.0: {} - - lodash.isarguments@3.1.0: {} - lodash.merge@4.6.2: {} lodash.throttle@4.1.1: {} - lodash@4.17.23: {} + lodash@4.18.1: {} log-symbols@4.1.0: dependencies: @@ -9582,7 +10730,7 @@ snapshots: logkitty@0.7.1: dependencies: ansi-fragments: 0.2.1 - dayjs: 1.11.20 + dayjs: 1.11.21 yargs: 15.4.1 loose-envify@1.4.0: @@ -9591,7 +10739,7 @@ snapshots: loupe@3.2.1: {} - lru-cache@11.2.6: {} + lru-cache@11.5.1: {} lru-cache@5.1.1: dependencies: @@ -9603,7 +10751,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.4 + semver: 7.8.1 makeerror@1.0.12: dependencies: @@ -9629,50 +10777,51 @@ snapshots: merge2@1.4.1: {} - metro-babel-transformer@0.83.5: + metro-babel-transformer@0.83.7: dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.7 flow-enums-runtime: 0.0.6 - hermes-parser: 0.33.3 + hermes-parser: 0.35.0 + metro-cache-key: 0.83.7 nullthrows: 1.1.1 transitivePeerDependencies: - supports-color - metro-cache-key@0.83.5: + metro-cache-key@0.83.7: dependencies: flow-enums-runtime: 0.0.6 - metro-cache@0.83.5: + metro-cache@0.83.7: dependencies: exponential-backoff: 3.1.3 flow-enums-runtime: 0.0.6 https-proxy-agent: 7.0.6 - metro-core: 0.83.5 + metro-core: 0.83.7 transitivePeerDependencies: - supports-color - metro-config@0.83.5: + metro-config@0.83.7: dependencies: connect: 3.7.0 flow-enums-runtime: 0.0.6 jest-validate: 29.7.0 - metro: 0.83.5 - metro-cache: 0.83.5 - metro-core: 0.83.5 - metro-runtime: 0.83.5 - yaml: 2.8.2 + metro: 0.83.7 + metro-cache: 0.83.7 + metro-core: 0.83.7 + metro-runtime: 0.83.7 + yaml: 2.9.0 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - metro-core@0.83.5: + metro-core@0.83.7: dependencies: flow-enums-runtime: 0.0.6 lodash.throttle: 4.1.1 - metro-resolver: 0.83.5 + metro-resolver: 0.83.7 - metro-file-map@0.83.5: + metro-file-map@0.83.7: dependencies: debug: 4.4.3 fb-watchman: 2.0.2 @@ -9686,117 +10835,116 @@ snapshots: transitivePeerDependencies: - supports-color - metro-minify-terser@0.83.5: + metro-minify-terser@0.83.7: dependencies: flow-enums-runtime: 0.0.6 - terser: 5.46.0 + terser: 5.48.0 - metro-resolver@0.83.5: + metro-resolver@0.83.7: dependencies: flow-enums-runtime: 0.0.6 - metro-runtime@0.83.5: + metro-runtime@0.83.7: dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.7 flow-enums-runtime: 0.0.6 - metro-source-map@0.83.5: + metro-source-map@0.83.7: dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 flow-enums-runtime: 0.0.6 invariant: 2.2.4 - metro-symbolicate: 0.83.5 + metro-symbolicate: 0.83.7 nullthrows: 1.1.1 - ob1: 0.83.5 + ob1: 0.83.7 source-map: 0.5.7 vlq: 1.0.1 transitivePeerDependencies: - supports-color - metro-symbolicate@0.83.5: + metro-symbolicate@0.83.7: dependencies: flow-enums-runtime: 0.0.6 invariant: 2.2.4 - metro-source-map: 0.83.5 + metro-source-map: 0.83.7 nullthrows: 1.1.1 source-map: 0.5.7 vlq: 1.0.1 transitivePeerDependencies: - supports-color - metro-transform-plugins@0.83.5: + metro-transform-plugins@0.83.7: dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 flow-enums-runtime: 0.0.6 nullthrows: 1.1.1 transitivePeerDependencies: - supports-color - metro-transform-worker@0.83.5: + metro-transform-worker@0.83.7: dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 + '@babel/core': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 flow-enums-runtime: 0.0.6 - metro: 0.83.5 - metro-babel-transformer: 0.83.5 - metro-cache: 0.83.5 - metro-cache-key: 0.83.5 - metro-minify-terser: 0.83.5 - metro-source-map: 0.83.5 - metro-transform-plugins: 0.83.5 + metro: 0.83.7 + metro-babel-transformer: 0.83.7 + metro-cache: 0.83.7 + metro-cache-key: 0.83.7 + metro-minify-terser: 0.83.7 + metro-source-map: 0.83.7 + metro-transform-plugins: 0.83.7 nullthrows: 1.1.1 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - metro@0.83.5: + metro@0.83.7: dependencies: - '@babel/code-frame': 7.29.0 - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/core': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 accepts: 2.0.0 - chalk: 4.1.2 ci-info: 2.0.0 connect: 3.7.0 debug: 4.4.3 error-stack-parser: 2.1.4 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 - hermes-parser: 0.33.3 + hermes-parser: 0.35.0 image-size: 1.2.1 invariant: 2.2.4 jest-worker: 29.7.0 jsc-safe-url: 0.2.4 lodash.throttle: 4.1.1 - metro-babel-transformer: 0.83.5 - metro-cache: 0.83.5 - metro-cache-key: 0.83.5 - metro-config: 0.83.5 - metro-core: 0.83.5 - metro-file-map: 0.83.5 - metro-resolver: 0.83.5 - metro-runtime: 0.83.5 - metro-source-map: 0.83.5 - metro-symbolicate: 0.83.5 - metro-transform-plugins: 0.83.5 - metro-transform-worker: 0.83.5 + metro-babel-transformer: 0.83.7 + metro-cache: 0.83.7 + metro-cache-key: 0.83.7 + metro-config: 0.83.7 + metro-core: 0.83.7 + metro-file-map: 0.83.7 + metro-resolver: 0.83.7 + metro-runtime: 0.83.7 + metro-source-map: 0.83.7 + metro-symbolicate: 0.83.7 + metro-transform-plugins: 0.83.7 + metro-transform-worker: 0.83.7 mime-types: 3.0.2 nullthrows: 1.1.1 serialize-error: 2.1.0 source-map: 0.5.7 throat: 5.0.0 - ws: 7.5.10 + ws: 7.5.11 yargs: 17.7.2 transitivePeerDependencies: - bufferutil @@ -9806,7 +10954,7 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 + picomatch: 2.3.2 mime-db@1.52.0: {} @@ -9830,13 +10978,13 @@ snapshots: minimalistic-assert@1.0.1: {} - minimatch@10.2.4: + minimatch@10.2.5: dependencies: - brace-expansion: 5.0.4 + brace-expansion: 5.0.6 minimatch@3.1.5: dependencies: - brace-expansion: 1.1.12 + brace-expansion: 1.1.15 minimist@1.2.8: {} @@ -9848,7 +10996,7 @@ snapshots: dependencies: obliterator: 2.0.5 - mnemonist@0.40.3: + mnemonist@0.40.4: dependencies: obliterator: 2.0.5 @@ -9860,7 +11008,9 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.11: {} + nanoid@3.3.12: {} + + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} @@ -9887,7 +11037,7 @@ snapshots: node-int64@0.4.0: {} - node-releases@2.0.36: {} + node-releases@2.0.46: {} node-stream-zip@1.15.0: {} @@ -9903,13 +11053,13 @@ snapshots: nullthrows@1.1.1: {} - nypm@0.6.5: + nypm@0.6.6: dependencies: - citty: 0.2.1 + citty: 0.2.2 pathe: 2.0.3 - tinyexec: 1.0.2 + tinyexec: 1.2.3 - ob1@0.83.5: + ob1@0.83.7: dependencies: flow-enums-runtime: 0.0.6 @@ -9921,33 +11071,33 @@ snapshots: object.assign@4.1.7: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 has-symbols: 1.1.0 object-keys: 1.1.1 object.entries@1.1.9: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 object.fromentries@2.0.8: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.1 - es-object-atoms: 1.1.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 object.values@1.2.1: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 obliterator@2.0.5: {} @@ -10037,7 +11187,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -10054,7 +11204,7 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.2.6 + lru-cache: 11.5.1 minipass: 7.1.3 pathe@1.1.2: {} @@ -10067,9 +11217,9 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: {} + picomatch@2.3.2: {} - picomatch@4.0.3: {} + picomatch@4.0.4: {} pino-abstract-transport@3.0.0: dependencies: @@ -10079,7 +11229,7 @@ snapshots: dependencies: colorette: 2.0.20 dateformat: 4.6.3 - fast-copy: 4.0.2 + fast-copy: 4.0.3 fast-safe-stringify: 2.1.1 help-me: 5.0.0 joycon: 3.1.1 @@ -10105,7 +11255,7 @@ snapshots: real-require: 0.2.0 safe-stable-stringify: 2.5.0 sonic-boom: 4.2.1 - thread-stream: 4.0.0 + thread-stream: 4.2.0 pirates@4.0.7: {} @@ -10113,21 +11263,23 @@ snapshots: dependencies: find-up: 4.1.0 - pkg-types@2.3.0: + pkg-types@2.3.1: dependencies: confbox: 0.2.4 exsolve: 1.0.8 pathe: 2.0.3 + pluralize@8.0.0: {} + pngjs@5.0.0: {} possible-typed-array-names@1.1.0: {} postcss-value-parser@4.2.0: {} - postcss@8.5.8: + postcss@8.5.15: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -10141,10 +11293,10 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - prisma@6.19.2(typescript@5.9.3): + prisma@6.19.3(typescript@5.9.3): dependencies: - '@prisma/config': 6.19.2 - '@prisma/engines': 6.19.2 + '@prisma/config': 6.19.3 + '@prisma/engines': 6.19.3 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -10188,7 +11340,7 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 - qs@6.14.2: + qs@6.15.2: dependencies: side-channel: 1.1.0 @@ -10218,18 +11370,18 @@ snapshots: rc9@2.1.2: dependencies: - defu: 6.1.4 + defu: 6.1.7 destr: 2.0.5 react-devtools-core@6.1.5: dependencies: - shell-quote: 1.8.3 - ws: 7.5.10 + shell-quote: 1.8.4 + ws: 7.5.11 transitivePeerDependencies: - bufferutil - utf-8-validate - react-dom@19.2.4(react@19.2.3): + react-dom@19.2.6(react@19.2.3): dependencies: react: 19.2.3 scheduler: 0.27.0 @@ -10242,79 +11394,89 @@ snapshots: react-is@18.3.1: {} - react-is@19.2.4: {} + react-is@19.2.6: {} + + react-native-camera-kit@14.2.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) - react-native-gesture-handler@2.31.2(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): + react-native-gesture-handler@2.31.2(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3): dependencies: '@egjs/hammerjs': 2.0.17 '@types/react-test-renderer': 19.1.0 hoist-non-react-statics: 3.3.2 invariant: 2.2.4 react: 19.2.3 - react-native: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) - react-native-is-edge-to-edge@1.1.7(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): + react-native-is-edge-to-edge@1.1.7(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) - react-native-qrcode-svg@6.3.21(react-native-svg@15.15.3(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): + react-native-qrcode-svg@6.3.21(react-native-svg@15.15.5(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3): dependencies: prop-types: 15.8.1 qrcode: 1.5.4 react: 19.2.3 - react-native: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) - react-native-svg: 15.15.3(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) + react-native-svg: 15.15.5(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) text-encoding: 0.7.0 - react-native-reanimated@3.19.5(@babel/core@7.29.0)(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): - dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) - '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) + react-native-reanimated@3.19.5(@babel/core@7.29.7)(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-shorthand-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7) + '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) convert-source-map: 2.0.0 invariant: 2.2.4 react: 19.2.3 - react-native: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) - react-native-is-edge-to-edge: 1.1.7(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) + react-native-is-edge-to-edge: 1.1.7(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) transitivePeerDependencies: - supports-color - react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): + react-native-safe-area-context@5.8.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) - react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): + react-native-screens@4.25.2(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 react-freeze: 1.0.4(react@19.2.3) - react-native: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) warn-once: 0.1.1 - react-native-svg@15.15.3(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): + react-native-svg@15.15.5(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3): dependencies: css-select: 5.2.2 css-tree: 1.1.3 react: 19.2.3 - react-native: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) - warn-once: 0.1.1 + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) react-native-vector-icons@10.3.0: dependencies: prop-types: 15.8.1 yargs: 16.2.0 - react-native-web@0.21.2(react-dom@19.2.4(react@19.2.3))(react@19.2.3): + react-native-view-shot@5.1.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3): + dependencies: + html2canvas: 1.4.1 + react: 19.2.3 + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) + + react-native-web@0.21.2(react-dom@19.2.6(react@19.2.3))(react@19.2.3): dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.7 '@react-native/normalize-colors': 0.74.89 fbjs: 3.0.5 inline-style-prefixer: 7.0.1 @@ -10322,32 +11484,51 @@ snapshots: nullthrows: 1.1.1 postcss-value-parser: 4.2.0 react: 19.2.3 - react-dom: 19.2.4(react@19.2.3) + react-dom: 19.2.6(react@19.2.3) styleq: 0.1.3 transitivePeerDependencies: - encoding - react-native-webview@13.16.1(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): + react-native-webview@13.16.1(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3): dependencies: escape-string-regexp: 4.0.0 invariant: 2.2.4 react: 19.2.3 - react-native: 0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3) + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) + + react-native-worklets@0.5.1(@babel/core@7.29.7)(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-shorthand-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7) + '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) + convert-source-map: 2.0.0 + react: 19.2.3 + react-native: 0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) + semver: 7.7.2 + transitivePeerDependencies: + - supports-color - react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3): + react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3): dependencies: '@jest/create-cache-key-function': 29.7.0 '@react-native/assets-registry': 0.84.1 - '@react-native/codegen': 0.84.1(@babel/core@7.29.0) - '@react-native/community-cli-plugin': 0.84.1(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0)) + '@react-native/codegen': 0.84.1(@babel/core@7.29.7) + '@react-native/community-cli-plugin': 0.84.1(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7)) '@react-native/gradle-plugin': 0.84.1 '@react-native/js-polyfills': 0.84.1 '@react-native/normalize-colors': 0.84.1 - '@react-native/virtualized-lists': 0.84.1(@types/react@19.2.14)(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + '@react-native/virtualized-lists': 0.84.1(@types/react@19.2.15)(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 - babel-jest: 29.7.0(@babel/core@7.29.0) + babel-jest: 29.7.0(@babel/core@7.29.7) babel-plugin-syntax-hermes-parser: 0.32.0 base64-js: 1.5.1 commander: 12.1.0 @@ -10356,8 +11537,8 @@ snapshots: invariant: 2.2.4 jest-environment-node: 29.7.0 memoize-one: 5.2.1 - metro-runtime: 0.83.5 - metro-source-map: 0.83.5 + metro-runtime: 0.83.7 + metro-source-map: 0.83.7 nullthrows: 1.1.1 pretty-format: 29.7.0 promise: 8.3.0 @@ -10366,14 +11547,14 @@ snapshots: react-refresh: 0.14.2 regenerator-runtime: 0.13.11 scheduler: 0.27.0 - semver: 7.7.4 + semver: 7.8.1 stacktrace-parser: 0.1.11 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 whatwg-fetch: 3.6.20 - ws: 7.5.10 + ws: 7.5.11 yargs: 17.7.2 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 transitivePeerDependencies: - '@babel/core' - '@react-native-community/cli' @@ -10387,7 +11568,7 @@ snapshots: react-test-renderer@19.2.3(react@19.2.3): dependencies: react: 19.2.3 - react-is: 19.2.4 + react-is: 19.2.6 scheduler: 0.27.0 react@19.2.3: {} @@ -10402,6 +11583,8 @@ snapshots: real-require@0.2.0: {} + real-require@1.0.0: {} + redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -10410,11 +11593,11 @@ snapshots: reflect.getprototypeof@1.0.10: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-intrinsic: 1.3.0 get-proto: 1.0.1 which-builtin-type: 1.2.1 @@ -10427,9 +11610,11 @@ snapshots: regenerator-runtime@0.13.11: {} + regexp-tree@0.1.27: {} + regexp.prototype.flags@1.5.4: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 es-errors: 1.3.0 get-proto: 1.0.1 @@ -10441,13 +11626,13 @@ snapshots: regenerate: 1.4.2 regenerate-unicode-properties: 10.2.2 regjsgen: 0.8.0 - regjsparser: 0.13.0 + regjsparser: 0.13.1 unicode-match-property-ecmascript: 2.0.0 unicode-match-property-value-ecmascript: 2.2.1 regjsgen@0.8.0: {} - regjsparser@0.13.0: + regjsparser@0.13.1: dependencies: jsesc: 3.1.0 @@ -10469,16 +11654,17 @@ snapshots: resolve.exports@2.0.3: {} - resolve@1.22.11: + resolve@1.22.12: dependencies: - is-core-module: 2.16.1 + es-errors: 1.3.0 + is-core-module: 2.16.2 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@2.0.0-next.6: + resolve@2.0.0-next.7: dependencies: es-errors: 1.3.0 - is-core-module: 2.16.1 + is-core-module: 2.16.2 node-exports-info: 1.6.0 object-keys: 1.1.1 path-parse: 1.0.7 @@ -10499,35 +11685,35 @@ snapshots: dependencies: glob: 7.2.3 - rollup@4.59.0: + rollup@4.60.4: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.59.0 - '@rollup/rollup-android-arm64': 4.59.0 - '@rollup/rollup-darwin-arm64': 4.59.0 - '@rollup/rollup-darwin-x64': 4.59.0 - '@rollup/rollup-freebsd-arm64': 4.59.0 - '@rollup/rollup-freebsd-x64': 4.59.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 - '@rollup/rollup-linux-arm-musleabihf': 4.59.0 - '@rollup/rollup-linux-arm64-gnu': 4.59.0 - '@rollup/rollup-linux-arm64-musl': 4.59.0 - '@rollup/rollup-linux-loong64-gnu': 4.59.0 - '@rollup/rollup-linux-loong64-musl': 4.59.0 - '@rollup/rollup-linux-ppc64-gnu': 4.59.0 - '@rollup/rollup-linux-ppc64-musl': 4.59.0 - '@rollup/rollup-linux-riscv64-gnu': 4.59.0 - '@rollup/rollup-linux-riscv64-musl': 4.59.0 - '@rollup/rollup-linux-s390x-gnu': 4.59.0 - '@rollup/rollup-linux-x64-gnu': 4.59.0 - '@rollup/rollup-linux-x64-musl': 4.59.0 - '@rollup/rollup-openbsd-x64': 4.59.0 - '@rollup/rollup-openharmony-arm64': 4.59.0 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-ia32-msvc': 4.59.0 - '@rollup/rollup-win32-x64-gnu': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.59.0 + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 fsevents: 2.3.3 run-parallel@1.2.0: @@ -10542,9 +11728,9 @@ snapshots: dependencies: mri: 1.2.0 - safe-array-concat@1.1.3: + safe-array-concat@1.1.4: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 get-intrinsic: 1.3.0 has-symbols: 1.1.0 @@ -10563,10 +11749,14 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 - safe-regex2@5.0.0: + safe-regex2@5.1.1: dependencies: ret: 0.5.0 + safe-regex@2.1.1: + dependencies: + regexp-tree: 0.1.27 + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} @@ -10577,7 +11767,9 @@ snapshots: semver@6.3.1: {} - semver@7.7.4: {} + semver@7.7.2: {} + + semver@7.8.1: {} send@0.19.2: dependencies: @@ -10612,7 +11804,7 @@ snapshots: set-cookie-parser@2.7.2: {} - set-cookie-parser@3.0.1: {} + set-cookie-parser@3.1.0: {} set-function-length@1.2.2: dependencies: @@ -10634,7 +11826,7 @@ snapshots: dependencies: dunder-proto: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 setimmediate@1.0.5: {} @@ -10650,7 +11842,9 @@ snapshots: shell-quote@1.8.3: {} - side-channel-list@1.0.0: + shell-quote@1.8.4: {} + + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 @@ -10674,7 +11868,7 @@ snapshots: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 - side-channel-list: 1.0.0 + side-channel-list: 1.0.1 side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 @@ -10730,6 +11924,8 @@ snapshots: sprintf-js@1.0.3: {} + stable-hash-x@0.2.0: {} + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -10780,12 +11976,12 @@ snapshots: string.prototype.matchall@4.0.12: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-intrinsic: 1.3.0 gopd: 1.2.0 has-symbols: 1.1.0 @@ -10797,30 +11993,30 @@ snapshots: string.prototype.repeat@1.0.0: dependencies: define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 string.prototype.trim@1.2.10: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.24.1 - es-object-atoms: 1.1.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 has-property-descriptors: 1.0.2 string.prototype.trimend@1.0.9: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 string.prototype.trimstart@1.0.8: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 string_decoder@1.3.0: dependencies: @@ -10838,6 +12034,8 @@ snapshots: strip-final-newline@2.0.0: {} + strip-indent@4.1.1: {} + strip-json-comments@3.1.1: {} strip-json-comments@5.0.3: {} @@ -10856,38 +12054,42 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@4.4.5(picomatch@4.0.3)(svelte@5.53.10)(typescript@5.9.3): + svelte-check@4.4.8(picomatch@4.0.4)(svelte@5.56.0(@typescript-eslint/types@8.60.0))(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 - fdir: 6.5.0(picomatch@4.0.3) + fdir: 6.5.0(picomatch@4.0.4) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.53.10 + svelte: 5.56.0(@typescript-eslint/types@8.60.0) typescript: 5.9.3 transitivePeerDependencies: - picomatch - svelte@5.53.10: + svelte@5.56.0(@typescript-eslint/types@8.60.0): dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@types/estree': 1.0.8 + '@sveltejs/acorn-typescript': 1.0.10(acorn@8.16.0) + '@types/estree': 1.0.9 '@types/trusted-types': 2.0.7 acorn: 8.16.0 aria-query: 5.3.1 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.6.4 + devalue: 5.8.1 esm-env: 1.2.2 - esrap: 2.2.3 + esrap: 2.2.9(@typescript-eslint/types@8.60.0) is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' - terser@5.46.0: + tapable@2.3.3: {} + + terser@5.48.0: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.16.0 @@ -10896,17 +12098,21 @@ snapshots: test-exclude@6.0.0: dependencies: - '@istanbuljs/schema': 0.1.3 + '@istanbuljs/schema': 0.1.6 glob: 7.2.3 minimatch: 3.1.5 text-encoding@0.7.0: {} + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + text-table@0.2.0: {} - thread-stream@4.0.0: + thread-stream@4.2.0: dependencies: - real-require: 0.2.0 + real-require: 1.0.0 throat@5.0.0: {} @@ -10914,12 +12120,12 @@ snapshots: tinyexec@0.3.2: {} - tinyexec@1.0.2: {} + tinyexec@1.2.3: {} - tinyglobby@0.2.15: + tinyglobby@0.2.16: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tinypool@1.1.1: {} @@ -10933,7 +12139,7 @@ snapshots: dependencies: is-number: 7.0.0 - toad-cache@3.7.0: {} + toad-cache@3.7.1: {} toidentifier@1.0.1: {} @@ -10943,16 +12149,15 @@ snapshots: tree-kill@1.2.2: {} - ts-api-utils@2.4.0(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 tslib@2.8.1: {} - tsx@4.21.0: + tsx@4.22.3: dependencies: - esbuild: 0.27.3 - get-tsconfig: 4.13.6 + esbuild: 0.28.0 optionalDependencies: fsevents: 2.3.3 @@ -10981,7 +12186,7 @@ snapshots: typed-array-byte-length@1.0.3: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 for-each: 0.3.5 gopd: 1.2.0 has-proto: 1.2.0 @@ -10990,22 +12195,33 @@ snapshots: typed-array-byte-offset@1.0.4: dependencies: available-typed-arrays: 1.0.7 - call-bind: 1.0.8 + call-bind: 1.0.9 for-each: 0.3.5 gopd: 1.2.0 has-proto: 1.2.0 is-typed-array: 1.1.15 reflect.getprototypeof: 1.0.10 - typed-array-length@1.0.7: + typed-array-length@1.0.8: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 for-each: 0.3.5 gopd: 1.2.0 is-typed-array: 1.1.15 possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typescript-eslint@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + eslint: 10.4.1(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} ua-parser-js@1.0.41: {} @@ -11034,9 +12250,36 @@ snapshots: unpipe@1.0.0: {} - update-browserslist-db@1.2.3(browserslist@4.28.1): + unrs-resolver@1.12.2: dependencies: - browserslist: 4.28.1 + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 escalade: 3.2.0 picocolors: 1.1.1 @@ -11056,6 +12299,10 @@ snapshots: utils-merge@1.0.1: {} + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + v8-to-istanbul@9.3.0: dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -11064,13 +12311,13 @@ snapshots: vary@1.1.2: {} - vite-node@2.1.9(@types/node@22.19.15)(terser@5.46.0): + vite-node@2.1.9(@types/node@22.19.19)(terser@5.48.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 1.1.2 - vite: 5.4.21(@types/node@22.19.15)(terser@5.46.0) + vite: 5.4.21(@types/node@22.19.19)(terser@5.48.0) transitivePeerDependencies: - '@types/node' - less @@ -11082,40 +12329,40 @@ snapshots: - supports-color - terser - vite@5.4.21(@types/node@22.19.15)(terser@5.46.0): + vite@5.4.21(@types/node@22.19.19)(terser@5.48.0): dependencies: esbuild: 0.21.5 - postcss: 8.5.8 - rollup: 4.59.0 + postcss: 8.5.15 + rollup: 4.60.4 optionalDependencies: - '@types/node': 22.19.15 + '@types/node': 22.19.19 fsevents: 2.3.3 - terser: 5.46.0 + terser: 5.48.0 - vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0): dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.8 - rollup: 4.59.0 - tinyglobby: 0.2.15 + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.60.4 + tinyglobby: 0.2.16 optionalDependencies: - '@types/node': 22.19.15 + '@types/node': 22.19.19 fsevents: 2.3.3 - jiti: 2.6.1 - terser: 5.46.0 - tsx: 4.21.0 - yaml: 2.8.2 + jiti: 2.7.0 + terser: 5.48.0 + tsx: 4.22.3 + yaml: 2.9.0 - vitefu@1.1.2(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + vitefu@1.1.3(vite@7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0)): optionalDependencies: - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.3(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0) - vitest@2.1.9(@types/node@22.19.15)(terser@5.46.0): + vitest@2.1.9(@types/node@22.19.19)(terser@5.48.0): dependencies: '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.15)(terser@5.46.0)) + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.19)(terser@5.48.0)) '@vitest/pretty-format': 2.1.9 '@vitest/runner': 2.1.9 '@vitest/snapshot': 2.1.9 @@ -11131,11 +12378,11 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.1.1 tinyrainbow: 1.2.0 - vite: 5.4.21(@types/node@22.19.15)(terser@5.46.0) - vite-node: 2.1.9(@types/node@22.19.15)(terser@5.46.0) + vite: 5.4.21(@types/node@22.19.19)(terser@5.48.0) + vite-node: 2.1.9(@types/node@22.19.19)(terser@5.48.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.19.15 + '@types/node': 22.19.19 transitivePeerDependencies: - less - lightningcss @@ -11190,7 +12437,7 @@ snapshots: isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.20 + which-typed-array: 1.1.21 which-collection@1.0.2: dependencies: @@ -11201,10 +12448,10 @@ snapshots: which-module@2.0.1: {} - which-typed-array@1.1.20: + which-typed-array@1.1.21: dependencies: available-typed-arrays: 1.0.7 - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 for-each: 0.3.5 get-proto: 1.0.1 @@ -11241,11 +12488,11 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 - ws@6.2.3: + ws@6.2.4: dependencies: async-limiter: 1.0.1 - ws@7.5.10: {} + ws@7.5.11: {} xtend@4.0.2: {} @@ -11255,7 +12502,7 @@ snapshots: yallist@3.1.1: {} - yaml@2.8.2: {} + yaml@2.9.0: {} yargs-parser@18.1.3: dependencies: