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.
+
+
+
+
+
+
+**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 @@
+
+
+
@@ -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** 🚀
+
+
+
+
+
+
+
+
+
+## Project Support
+
+
+
+
+
+
+
+
+
+
+
+---
+
## 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.
-
-
-
-
-
-
-
-
-
⚡ DevCard
-
- {theme === 'light' ? '🌙' : '☀️'}
-
-
-
-
-
- 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.
-
-
-
-
-
-
+
\ 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 @@
{/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: