From 3554aa6f19e71172ddaa25c876ed1b363d9895cf Mon Sep 17 00:00:00 2001 From: coder-soft Date: Sun, 12 Jul 2026 15:31:09 +0500 Subject: [PATCH 1/3] Updated some stuff --- public/albums_test.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/albums_test.txt diff --git a/public/albums_test.txt b/public/albums_test.txt new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/public/albums_test.txt @@ -0,0 +1 @@ +test From 31141fb5fdd2a2f6b6043326a30e1695a1939185 Mon Sep 17 00:00:00 2001 From: coder-soft Date: Sun, 26 Jul 2026 11:43:26 +0500 Subject: [PATCH 2/3] feat: integrate wisp analytics with Supabase auth - Update @renderdragonorg/wisp from 0.1.1 to 0.2.0 - Replace custom fetch transport with built-in wispSecret config option - Add bindSupabase() to auto-link auth state and enrich machine records with user email, name, and auth provider - Add userEmail, userName, authProvider fields to machines Convex schema - Handle session_identify events in analytics ingestion pipeline - Surface enriched user fields in dashboard queries (machine stats, page visitors, machine search) - Remove manual wisp.identify/reset from AuthProvider (now handled by bindSupabase) --- convex/dashboard.ts | 9 + convex/events.ts | 25 ++- convex/schema.ts | 3 + package.json | 2 +- pnpm-lock.yaml | 358 +++++++++++++++++---------------- pnpm-workspace.yaml | 2 +- src/main.tsx | 26 +-- src/providers/AuthProvider.tsx | 20 -- supabase.md | 148 ++++++++++++++ 9 files changed, 369 insertions(+), 224 deletions(-) create mode 100644 supabase.md diff --git a/convex/dashboard.ts b/convex/dashboard.ts index 1ecb1ac..a9bee3a 100644 --- a/convex/dashboard.ts +++ b/convex/dashboard.ts @@ -280,6 +280,9 @@ export const getMachineStats = query({ region: machine.region ?? null, city: machine.city ?? null, screen: machine.screen ?? null, + userEmail: machine.userEmail ?? null, + userName: machine.userName ?? null, + authProvider: machine.authProvider ?? null, }, stats: { totalSessions, @@ -381,6 +384,9 @@ export const getPageVisitors = query({ country: machine?.country ?? null, platform: machine?.platform ?? null, userAgent: machine?.userAgent ?? null, + userEmail: machine?.userEmail ?? null, + userName: machine?.userName ?? null, + authProvider: machine?.authProvider ?? null, }; }) ); @@ -433,6 +439,9 @@ export const searchMachines = query({ country: m.country ?? null, platform: m.platform ?? null, lastSeenAt: m.lastSeenAt, + userEmail: m.userEmail ?? null, + userName: m.userName ?? null, + authProvider: m.authProvider ?? null, })); }, }); diff --git a/convex/events.ts b/convex/events.ts index dbbc91b..8d18307 100644 --- a/convex/events.ts +++ b/convex/events.ts @@ -28,6 +28,7 @@ async function upsertMachine( timestamp: number; meta?: Record; geo?: { ip?: string; country?: string; region?: string; city?: string }; + userInfo?: { email?: string; name?: string; provider?: string }; } ) { const existing = await ctx.db @@ -51,6 +52,9 @@ async function upsertMachine( country: args.geo?.country, region: args.geo?.region, city: args.geo?.city, + userEmail: args.userInfo?.email, + userName: args.userInfo?.name, + authProvider: args.userInfo?.provider, }); return { isNewMachine: true }; } @@ -62,6 +66,9 @@ async function upsertMachine( platform: args.meta?.platform ?? existing.platform, referrer: args.meta?.referrer ?? existing.referrer, screen: args.meta?.screen ?? existing.screen, + userEmail: args.userInfo?.email ?? existing.userEmail, + userName: args.userInfo?.name ?? existing.userName, + authProvider: args.userInfo?.provider ?? existing.authProvider, }); return { isNewMachine: false }; } @@ -122,20 +129,32 @@ async function upsertSession( } } +function extractUserInfo(events: Array<{ name: string; payload?: unknown }>): { email?: string; name?: string; provider?: string } | undefined { + const identifyEvent = events.find((e) => e.name === "session_identify"); + if (!identifyEvent?.payload || typeof identifyEvent.payload !== "object") return undefined; + const p = identifyEvent.payload as Record; + const email = typeof p.email === "string" ? p.email : undefined; + const name = typeof p.name === "string" ? p.name : undefined; + const provider = typeof p.provider === "string" ? p.provider : undefined; + return email || name || provider ? { email, name, provider } : undefined; +} + export const recordBatch = mutation({ args: { events: v.array(eventValidator) }, handler: async (ctx, { events }) => { // Process oldest-first so ordering-dependent bookkeeping (session creation, counters) is correct. const sorted = [...events].sort((a, b) => a.timestamp - b.timestamp); + const userInfo = extractUserInfo(sorted); for (const event of sorted) { - const isBookkeeping = event.name === "session_start"; + const isBookkeeping = event.name === "session_start" || event.name === "session_identify"; await upsertMachine(ctx, { machineId: event.machineId, userId: event.userId, timestamp: event.timestamp, meta: isBookkeeping ? event.payload : undefined, + userInfo, }); await upsertSession(ctx, { @@ -211,9 +230,10 @@ export const recordBatchWithGeo = internalMutation({ }, handler: async (ctx, { events, geo }) => { const sorted = [...events].sort((a, b) => a.timestamp - b.timestamp); + const userInfo = extractUserInfo(sorted); for (const event of sorted) { - const isBookkeeping = event.name === "session_start"; + const isBookkeeping = event.name === "session_start" || event.name === "session_identify"; await upsertMachine(ctx, { machineId: event.machineId, @@ -221,6 +241,7 @@ export const recordBatchWithGeo = internalMutation({ timestamp: event.timestamp, meta: isBookkeeping ? event.payload : undefined, geo, + userInfo, }); await upsertSession(ctx, { diff --git a/convex/schema.ts b/convex/schema.ts index cf38856..1a006ff 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -17,6 +17,9 @@ export default defineSchema({ region: v.optional(v.string()), city: v.optional(v.string()), screen: v.optional(v.string()), + userEmail: v.optional(v.string()), + userName: v.optional(v.string()), + authProvider: v.optional(v.string()), }) .index("by_machineId", ["machineId"]) .index("by_userId", ["userId"]) diff --git a/package.json b/package.json index f85b666..4da25cc 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@radix-ui/react-use-layout-effect": "^1.1.1", "@radix-ui/react-use-previous": "^1.1.1", "@radix-ui/react-visually-hidden": "^1.2.4", - "@renderdragonorg/wisp": "^0.1.1", + "@renderdragonorg/wisp": "^0.2.0", "@sentry/react": "^10.38.0", "@supabase/supabase-js": "^2.95.3", "@tabler/icons-react": "^3.36.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16d819b..295fe52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@distube/ytdl-core': specifier: ^4.16.12 - version: 4.16.12 + version: 4.16.12(supports-color@8.1.1) '@dnd-kit/core': specifier: ^6.3.1 version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -31,7 +31,7 @@ importers: version: 5.2.8 '@google/genai': specifier: ^0.13.0 - version: 0.13.0 + version: 0.13.0(supports-color@8.1.1) '@hcaptcha/react-hcaptcha': specifier: ^1.17.4 version: 1.17.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -150,8 +150,8 @@ importers: specifier: ^1.2.4 version: 1.2.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@renderdragonorg/wisp': - specifier: ^0.1.1 - version: 0.1.1(@supabase/supabase-js@2.98.0)(react@18.3.1) + specifier: ^0.2.0 + version: 0.2.0(@supabase/supabase-js@2.98.0)(react@18.3.1) '@sentry/react': specifier: ^10.38.0 version: 10.40.0(react@18.3.1) @@ -166,16 +166,16 @@ importers: version: 5.90.21(react@18.3.1) '@uploadthing/react': specifier: ^7.3.3 - version: 7.3.3(next@15.3.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(uploadthing@7.7.4(express@5.2.1)(next@15.3.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(tailwindcss@3.4.19(tsx@4.21.0))) + version: 7.3.3(next@15.3.8(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(uploadthing@7.7.4(express@5.2.1(supports-color@8.1.1))(next@15.3.8(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(tailwindcss@3.4.19(tsx@4.21.0))) '@vercel/analytics': specifier: ^1.6.1 - version: 1.6.1(next@15.3.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 1.6.1(next@15.3.8(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@vercel/node': specifier: ^5.5.33 - version: 5.6.9(rollup@4.59.0) + version: 5.6.9(rollup@4.59.0)(supports-color@8.1.1) '@vercel/speed-insights': specifier: ^1.3.1 - version: 1.3.1(next@15.3.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 1.3.1(next@15.3.8(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) aria-hidden: specifier: ^1.2.6 version: 1.2.6 @@ -211,7 +211,7 @@ importers: version: 8.6.0(react@18.3.1) express: specifier: ^5.2.1 - version: 5.2.1 + version: 5.2.1(supports-color@8.1.1) file-saver: specifier: ^2.0.5 version: 2.0.5 @@ -271,7 +271,7 @@ importers: version: 9.16.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-markdown: specifier: ^10.1.0 - version: 10.1.0(@types/react@18.3.28)(react@18.3.1) + version: 10.1.0(@types/react@18.3.28)(react@18.3.1)(supports-color@8.1.1) react-remove-scroll: specifier: ^2.7.2 version: 2.7.2(@types/react@18.3.28)(react@18.3.1) @@ -286,7 +286,7 @@ importers: version: 1.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) remark-gfm: specifier: ^4.0.1 - version: 4.0.1 + version: 4.0.1(supports-color@8.1.1) sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -298,7 +298,7 @@ importers: version: 1.0.7(tailwindcss@3.4.19(tsx@4.21.0)) uploadthing: specifier: ^7.7.4 - version: 7.7.4(express@5.2.1)(next@15.3.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(tailwindcss@3.4.19(tsx@4.21.0)) + version: 7.7.4(express@5.2.1(supports-color@8.1.1))(next@15.3.8(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(tailwindcss@3.4.19(tsx@4.21.0)) vaul: specifier: ^0.9.9 version: 0.9.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -320,7 +320,7 @@ importers: version: 9.39.3 '@sentry/vite-plugin': specifier: ^4.9.0 - version: 4.9.1 + version: 4.9.1(supports-color@8.1.1) '@tailwindcss/typography': specifier: ^0.5.19 version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)) @@ -359,13 +359,13 @@ importers: version: 1.42.1(react@18.3.1) eslint: specifier: ^9.39.2 - version: 9.39.3(jiti@1.21.7) + version: 9.39.3(jiti@1.21.7)(supports-color@8.1.1) eslint-plugin-react-hooks: specifier: ^5.2.0 - version: 5.2.0(eslint@9.39.3(jiti@1.21.7)) + version: 5.2.0(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1)) eslint-plugin-react-refresh: specifier: ^0.4.26 - version: 0.4.26(eslint@9.39.3(jiti@1.21.7)) + version: 0.4.26(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1)) globals: specifier: ^15.15.0 version: 15.15.0 @@ -380,7 +380,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.54.0 - version: 8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) + version: 8.56.1(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) vite: specifier: ^5.4.21 version: 5.4.21(@types/node@22.19.13) @@ -1959,8 +1959,8 @@ packages: resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==} engines: {node: '>=14.0.0'} - '@renderdragonorg/wisp@0.1.1': - resolution: {integrity: sha512-0nJJZABv/eP2+SC10gWtKkzZdNVIiQ7Eh1QaymYzF3C32EF7GUR3Fu6rWn2EWBo5+MQaLY+KNOCBDtZPz+rCvQ==} + '@renderdragonorg/wisp@0.2.0': + resolution: {integrity: sha512-xgT+Ti2xBXIKQharK4lHC95e4zJYJQMyEPBBA2s3uqwU0ec7edEgLpDiWHd7wSd8JRPua0jy+XOOEdGcF0/ERw==} peerDependencies: '@supabase/supabase-js': ^2.110.0 peerDependenciesMeta: @@ -5007,20 +5007,20 @@ snapshots: '@babel/compat-data@7.29.0': {} - '@babel/core@7.29.0': + '@babel/core@7.29.0(supports-color@8.1.1)': 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/helper-module-transforms': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helpers': 7.28.6 '@babel/parser': 7.29.0 '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.0 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -5045,19 +5045,19 @@ snapshots: '@babel/helper-globals@7.28.0': {} - '@babel/helper-module-imports@7.28.6': + '@babel/helper-module-imports@7.28.6(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-module-imports': 7.28.6(supports-color@8.1.1) '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -5084,7 +5084,7 @@ snapshots: '@babel/parser': 7.29.0 '@babel/types': 7.29.0 - '@babel/traverse@7.29.0': + '@babel/traverse@7.29.0(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.29.0 '@babel/generator': 7.29.1 @@ -5092,7 +5092,7 @@ snapshots: '@babel/parser': 7.29.0 '@babel/template': 7.28.6 '@babel/types': 7.29.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -5103,10 +5103,10 @@ snapshots: '@bytecodealliance/preview2-shim@0.17.6': {} - '@distube/ytdl-core@4.16.12': + '@distube/ytdl-core@4.16.12(supports-color@8.1.1)': dependencies: http-cookie-agent: 7.0.3(tough-cookie@5.1.2)(undici@7.22.0) - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@8.1.1) m3u8stream: 0.8.6 miniget: 4.2.3 sax: 1.4.4 @@ -5312,17 +5312,17 @@ snapshots: '@esbuild/win32-x64@0.27.0': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.3(jiti@1.21.7))': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1))': dependencies: - eslint: 9.39.3(jiti@1.21.7) + eslint: 9.39.3(jiti@1.21.7)(supports-color@8.1.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.1': + '@eslint/config-array@0.21.1(supports-color@8.1.1)': dependencies: '@eslint/object-schema': 2.1.7 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -5335,10 +5335,10 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.4': + '@eslint/eslintrc@3.3.4(supports-color@8.1.1)': dependencies: ajv: 6.14.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -5391,9 +5391,9 @@ snapshots: '@fontsource/jetbrains-mono@5.2.8': {} - '@google/genai@0.13.0': + '@google/genai@0.13.0(supports-color@8.1.1)': dependencies: - google-auth-library: 9.15.1 + google-auth-library: 9.15.1(supports-color@8.1.1) ws: 8.19.0 zod: 3.25.76 zod-to-json-schema: 3.25.1(zod@3.25.76) @@ -5576,11 +5576,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@mapbox/node-pre-gyp@2.0.3': + '@mapbox/node-pre-gyp@2.0.3(supports-color@8.1.1)': dependencies: consola: 3.4.2 detect-libc: 2.1.2 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@8.1.1) node-fetch: 2.6.9 nopt: 8.1.0 semver: 7.7.4 @@ -6421,7 +6421,7 @@ snapshots: '@remix-run/router@1.23.2': {} - '@renderdragonorg/wisp@0.1.1(@supabase/supabase-js@2.98.0)(react@18.3.1)': + '@renderdragonorg/wisp@0.2.0(@supabase/supabase-js@2.98.0)(react@18.3.1)': dependencies: convex: 1.42.1(react@18.3.1) optionalDependencies: @@ -6549,11 +6549,11 @@ snapshots: '@sentry-internal/replay-canvas': 10.40.0 '@sentry/core': 10.40.0 - '@sentry/bundler-plugin-core@4.9.1': + '@sentry/bundler-plugin-core@4.9.1(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@sentry/babel-plugin-component-annotate': 4.9.1 - '@sentry/cli': 2.58.5 + '@sentry/cli': 2.58.5(supports-color@8.1.1) dotenv: 16.6.1 find-up: 5.0.0 glob: 10.5.0 @@ -6587,9 +6587,9 @@ snapshots: '@sentry/cli-win32-x64@2.58.5': optional: true - '@sentry/cli@2.58.5': + '@sentry/cli@2.58.5(supports-color@8.1.1)': dependencies: - https-proxy-agent: 5.0.1 + https-proxy-agent: 5.0.1(supports-color@8.1.1) node-fetch: 2.7.0 progress: 2.0.3 proxy-from-env: 1.1.0 @@ -6615,9 +6615,9 @@ snapshots: '@sentry/core': 10.40.0 react: 18.3.1 - '@sentry/vite-plugin@4.9.1': + '@sentry/vite-plugin@4.9.1(supports-color@8.1.1)': dependencies: - '@sentry/bundler-plugin-core': 4.9.1 + '@sentry/bundler-plugin-core': 4.9.1(supports-color@8.1.1) unplugin: 1.0.1 transitivePeerDependencies: - encoding @@ -6864,15 +6864,15 @@ snapshots: dependencies: '@types/node': 22.19.13 - '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3))(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.56.1 - eslint: 9.39.3(jiti@1.21.7) + eslint: 9.39.3(jiti@1.21.7)(supports-color@8.1.1) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -6880,23 +6880,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.56.1 '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.1(supports-color@8.1.1)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.56.1 - debug: 4.4.3 - eslint: 9.39.3(jiti@1.21.7) + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.39.3(jiti@1.21.7)(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.56.1(supports-color@8.1.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) '@typescript-eslint/types': 8.56.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -6910,13 +6910,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.56.1(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) - debug: 4.4.3 - eslint: 9.39.3(jiti@1.21.7) + '@typescript-eslint/typescript-estree': 8.56.1(supports-color@8.1.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.39.3(jiti@1.21.7)(supports-color@8.1.1) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -6924,13 +6924,13 @@ snapshots: '@typescript-eslint/types@8.56.1': {} - '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.56.1(supports-color@8.1.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) + '@typescript-eslint/project-service': 8.56.1(supports-color@8.1.1)(typescript@5.9.3) '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) '@typescript-eslint/types': 8.56.1 '@typescript-eslint/visitor-keys': 8.56.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.4 semver: 7.7.4 tinyglobby: 0.2.15 @@ -6939,13 +6939,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1)) '@typescript-eslint/scope-manager': 8.56.1 '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - eslint: 9.39.3(jiti@1.21.7) + '@typescript-eslint/typescript-estree': 8.56.1(supports-color@8.1.1)(typescript@5.9.3) + eslint: 9.39.3(jiti@1.21.7)(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -6959,14 +6959,14 @@ snapshots: '@uploadthing/mime-types@0.3.6': {} - '@uploadthing/react@7.3.3(next@15.3.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(uploadthing@7.7.4(express@5.2.1)(next@15.3.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(tailwindcss@3.4.19(tsx@4.21.0)))': + '@uploadthing/react@7.3.3(next@15.3.8(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(uploadthing@7.7.4(express@5.2.1(supports-color@8.1.1))(next@15.3.8(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(tailwindcss@3.4.19(tsx@4.21.0)))': dependencies: '@uploadthing/shared': 7.1.10 file-selector: 0.6.0 react: 18.3.1 - uploadthing: 7.7.4(express@5.2.1)(next@15.3.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(tailwindcss@3.4.19(tsx@4.21.0)) + uploadthing: 7.7.4(express@5.2.1(supports-color@8.1.1))(next@15.3.8(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(tailwindcss@3.4.19(tsx@4.21.0)) optionalDependencies: - next: 15.3.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 15.3.8(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@uploadthing/shared@7.1.10': dependencies: @@ -6974,9 +6974,9 @@ snapshots: effect: 3.17.7 sqids: 0.3.0 - '@vercel/analytics@1.6.1(next@15.3.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@vercel/analytics@1.6.1(next@15.3.8(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': optionalDependencies: - next: 15.3.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 15.3.8(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 '@vercel/build-utils@13.6.1': @@ -6985,9 +6985,9 @@ snapshots: '@vercel/error-utils@2.0.3': {} - '@vercel/nft@1.1.1(rollup@4.59.0)': + '@vercel/nft@1.1.1(rollup@4.59.0)(supports-color@8.1.1)': dependencies: - '@mapbox/node-pre-gyp': 2.0.3 + '@mapbox/node-pre-gyp': 2.0.3(supports-color@8.1.1) '@rollup/pluginutils': 5.3.0(rollup@4.59.0) acorn: 8.16.0 acorn-import-attributes: 1.9.5(acorn@8.16.0) @@ -7004,7 +7004,7 @@ snapshots: - rollup - supports-color - '@vercel/node@5.6.9(rollup@4.59.0)': + '@vercel/node@5.6.9(rollup@4.59.0)(supports-color@8.1.1)': dependencies: '@edge-runtime/node-utils': 2.3.0 '@edge-runtime/primitives': 4.1.0 @@ -7012,7 +7012,7 @@ snapshots: '@types/node': 20.11.0 '@vercel/build-utils': 13.6.1 '@vercel/error-utils': 2.0.3 - '@vercel/nft': 1.1.1(rollup@4.59.0) + '@vercel/nft': 1.1.1(rollup@4.59.0)(supports-color@8.1.1) '@vercel/static-config': 3.1.2 async-listen: 3.0.0 cjs-module-lexer: 1.2.3 @@ -7044,9 +7044,9 @@ snapshots: smol-toml: 1.5.2 zod: 3.22.4 - '@vercel/speed-insights@1.3.1(next@15.3.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@vercel/speed-insights@1.3.1(next@15.3.8(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': optionalDependencies: - next: 15.3.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 15.3.8(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 '@vercel/static-config@3.1.2': @@ -7115,9 +7115,9 @@ snapshots: global: 4.4.0 pkcs7: 1.0.4 - agent-base@6.0.2: + agent-base@6.0.2(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -7205,11 +7205,11 @@ snapshots: dependencies: file-uri-to-path: 1.0.0 - body-parser@2.2.2: + body-parser@2.2.2(supports-color@8.1.1): dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 @@ -7408,9 +7408,11 @@ snapshots: date-fns@3.6.0: {} - debug@4.4.3: + debug@4.4.3(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 decode-named-character-reference@1.3.0: dependencies: @@ -7571,13 +7573,13 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-plugin-react-hooks@5.2.0(eslint@9.39.3(jiti@1.21.7)): + eslint-plugin-react-hooks@5.2.0(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1)): dependencies: - eslint: 9.39.3(jiti@1.21.7) + eslint: 9.39.3(jiti@1.21.7)(supports-color@8.1.1) - eslint-plugin-react-refresh@0.4.26(eslint@9.39.3(jiti@1.21.7)): + eslint-plugin-react-refresh@0.4.26(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1)): dependencies: - eslint: 9.39.3(jiti@1.21.7) + eslint: 9.39.3(jiti@1.21.7)(supports-color@8.1.1) eslint-scope@8.4.0: dependencies: @@ -7590,14 +7592,14 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.3(jiti@1.21.7): + eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 + '@eslint/config-array': 0.21.1(supports-color@8.1.1) '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.4 + '@eslint/eslintrc': 3.3.4(supports-color@8.1.1) '@eslint/js': 9.39.3 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.7 @@ -7607,7 +7609,7 @@ snapshots: ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -7657,20 +7659,20 @@ snapshots: event-target-shim@5.0.1: {} - express@5.2.1: + express@5.2.1(supports-color@8.1.1): dependencies: accepts: 2.0.0 - body-parser: 2.2.2 + body-parser: 2.2.2(supports-color@8.1.1) content-disposition: 1.0.1 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.1 + finalhandler: 2.1.1(supports-color@8.1.1) fresh: 2.0.0 http-errors: 2.0.1 merge-descriptors: 2.0.0 @@ -7681,9 +7683,9 @@ snapshots: proxy-addr: 2.0.7 qs: 6.15.0 range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 + router: 2.2.0(supports-color@8.1.1) + send: 1.2.1(supports-color@8.1.1) + serve-static: 2.2.1(supports-color@8.1.1) statuses: 2.0.2 type-is: 2.0.1 vary: 1.1.2 @@ -7736,9 +7738,9 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@2.1.1: + finalhandler@2.1.1(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -7814,10 +7816,10 @@ snapshots: fuse.js@6.6.2: {} - gaxios@6.7.1: + gaxios@6.7.1(supports-color@8.1.1): dependencies: extend: 3.0.2 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@8.1.1) is-stream: 2.0.1 node-fetch: 2.7.0 uuid: 9.0.1 @@ -7825,9 +7827,9 @@ snapshots: - encoding - supports-color - gcp-metadata@6.1.1: + gcp-metadata@6.1.1(supports-color@8.1.1): dependencies: - gaxios: 6.7.1 + gaxios: 6.7.1(supports-color@8.1.1) google-logging-utils: 0.0.2 json-bigint: 1.0.0 transitivePeerDependencies: @@ -7896,13 +7898,13 @@ snapshots: globals@15.15.0: {} - google-auth-library@9.15.1: + google-auth-library@9.15.1(supports-color@8.1.1): dependencies: base64-js: 1.5.1 ecdsa-sig-formatter: 1.0.11 - gaxios: 6.7.1 - gcp-metadata: 6.1.1 - gtoken: 7.1.0 + gaxios: 6.7.1(supports-color@8.1.1) + gcp-metadata: 6.1.1(supports-color@8.1.1) + gtoken: 7.1.0(supports-color@8.1.1) jws: 4.0.1 transitivePeerDependencies: - encoding @@ -7914,9 +7916,9 @@ snapshots: graceful-fs@4.2.11: {} - gtoken@7.1.0: + gtoken@7.1.0(supports-color@8.1.1): dependencies: - gaxios: 6.7.1 + gaxios: 6.7.1(supports-color@8.1.1) jws: 4.0.1 transitivePeerDependencies: - encoding @@ -7934,7 +7936,7 @@ snapshots: dependencies: function-bind: 1.1.2 - hast-util-to-jsx-runtime@2.3.6: + hast-util-to-jsx-runtime@2.3.6(supports-color@8.1.1): dependencies: '@types/estree': 1.0.8 '@types/hast': 3.0.4 @@ -7943,9 +7945,9 @@ snapshots: devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 + mdast-util-mdx-expression: 2.0.1(supports-color@8.1.1) + mdast-util-mdx-jsx: 3.2.0(supports-color@8.1.1) + mdast-util-mdxjs-esm: 2.0.1(supports-color@8.1.1) property-information: 7.1.0 space-separated-tokens: 2.0.2 style-to-js: 1.1.21 @@ -7984,17 +7986,17 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - https-proxy-agent@5.0.1: + https-proxy-agent@5.0.1(supports-color@8.1.1): dependencies: - agent-base: 6.0.2 - debug: 4.4.3 + agent-base: 6.0.2(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color - https-proxy-agent@7.0.6: + https-proxy-agent@7.0.6(supports-color@8.1.1): dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -8214,14 +8216,14 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - mdast-util-from-markdown@2.0.3: + mdast-util-from-markdown@2.0.3(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 decode-named-character-reference: 1.3.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 - micromark: 4.0.2 + micromark: 4.0.2(supports-color@8.1.1) micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-decode-string: 2.0.1 micromark-util-normalize-identifier: 2.0.1 @@ -8239,67 +8241,67 @@ snapshots: mdast-util-find-and-replace: 3.0.2 micromark-util-character: 2.1.1 - mdast-util-gfm-footnote@2.1.0: + mdast-util-gfm-footnote@2.1.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 micromark-util-normalize-identifier: 2.0.1 transitivePeerDependencies: - supports-color - mdast-util-gfm-strikethrough@2.0.0: + mdast-util-gfm-strikethrough@2.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm-table@2.0.0: + mdast-util-gfm-table@2.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 markdown-table: 3.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm-task-list-item@2.0.0: + mdast-util-gfm-task-list-item@2.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm@3.1.0: + mdast-util-gfm@3.1.0(supports-color@8.1.1): dependencies: - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-gfm-autolink-literal: 2.0.1 - mdast-util-gfm-footnote: 2.1.0 - mdast-util-gfm-strikethrough: 2.0.0 - mdast-util-gfm-table: 2.0.0 - mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-gfm-footnote: 2.1.0(supports-color@8.1.1) + mdast-util-gfm-strikethrough: 2.0.0(supports-color@8.1.1) + mdast-util-gfm-table: 2.0.0(supports-color@8.1.1) + mdast-util-gfm-task-list-item: 2.0.0(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdx-expression@2.0.1: + mdast-util-mdx-expression@2.0.1(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdx-jsx@3.2.0: + mdast-util-mdx-jsx@3.2.0(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 @@ -8307,7 +8309,7 @@ snapshots: '@types/unist': 3.0.3 ccount: 2.0.1 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 parse-entities: 4.0.2 stringify-entities: 4.0.4 @@ -8316,13 +8318,13 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-mdxjs-esm@2.0.1: + mdast-util-mdxjs-esm@2.0.1(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -8535,10 +8537,10 @@ snapshots: micromark-util-types@2.0.2: {} - micromark@4.0.2: + micromark@4.0.2(supports-color@8.1.1): dependencies: '@types/debug': 4.1.12 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) decode-named-character-reference: 1.3.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -8661,7 +8663,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@15.3.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@15.3.8(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 15.3.8 '@swc/counter': 0.1.3 @@ -8671,7 +8673,7 @@ snapshots: postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.6(@babel/core@7.29.0)(react@18.3.1) + styled-jsx: 5.1.6(@babel/core@7.29.0(supports-color@8.1.1))(react@18.3.1) optionalDependencies: '@next/swc-darwin-arm64': 15.3.5 '@next/swc-darwin-x64': 15.3.5 @@ -8971,17 +8973,17 @@ snapshots: react-is@16.13.1: {} - react-markdown@10.1.0(@types/react@18.3.28)(react@18.3.1): + react-markdown@10.1.0(@types/react@18.3.28)(react@18.3.1)(supports-color@8.1.1): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 '@types/react': 18.3.28 devlop: 1.1.0 - hast-util-to-jsx-runtime: 2.3.6 + hast-util-to-jsx-runtime: 2.3.6(supports-color@8.1.1) html-url-attributes: 3.0.1 mdast-util-to-hast: 13.2.1 react: 18.3.1 - remark-parse: 11.0.0 + remark-parse: 11.0.0(supports-color@8.1.1) remark-rehype: 11.1.2 unified: 11.0.5 unist-util-visit: 5.1.0 @@ -9065,21 +9067,21 @@ snapshots: dependencies: picomatch: 2.3.1 - remark-gfm@4.0.1: + remark-gfm@4.0.1(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-gfm: 3.1.0 + mdast-util-gfm: 3.1.0(supports-color@8.1.1) micromark-extension-gfm: 3.0.0 - remark-parse: 11.0.0 + remark-parse: 11.0.0(supports-color@8.1.1) remark-stringify: 11.0.0 unified: 11.0.5 transitivePeerDependencies: - supports-color - remark-parse@11.0.0: + remark-parse@11.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) micromark-util-types: 2.0.2 unified: 11.0.5 transitivePeerDependencies: @@ -9148,9 +9150,9 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 - router@2.2.0: + router@2.2.0(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -9182,9 +9184,9 @@ snapshots: semver@7.7.4: {} - send@1.2.1: + send@1.2.1(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -9198,12 +9200,12 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@2.2.1: + serve-static@2.2.1(supports-color@8.1.1): dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.1 + send: 1.2.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -9342,12 +9344,12 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - styled-jsx@5.1.6(@babel/core@7.29.0)(react@18.3.1): + styled-jsx@5.1.6(@babel/core@7.29.0(supports-color@8.1.1))(react@18.3.1): dependencies: client-only: 0.0.1 react: 18.3.1 optionalDependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) optional: true sucrase@3.35.1: @@ -9493,13 +9495,13 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 - typescript-eslint@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3): + typescript-eslint@8.56.1(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) - eslint: 9.39.3(jiti@1.21.7) + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3))(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.1(supports-color@8.1.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@1.21.7)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) + eslint: 9.39.3(jiti@1.21.7)(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -9566,7 +9568,7 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - uploadthing@7.7.4(express@5.2.1)(next@15.3.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(tailwindcss@3.4.19(tsx@4.21.0)): + uploadthing@7.7.4(express@5.2.1(supports-color@8.1.1))(next@15.3.8(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(tailwindcss@3.4.19(tsx@4.21.0)): dependencies: '@effect/platform': 0.90.3(effect@3.17.7) '@standard-schema/spec': 1.0.0-beta.4 @@ -9574,8 +9576,8 @@ snapshots: '@uploadthing/shared': 7.1.10 effect: 3.17.7 optionalDependencies: - express: 5.2.1 - next: 15.3.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + express: 5.2.1(supports-color@8.1.1) + next: 15.3.8(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwindcss: 3.4.19(tsx@4.21.0) uri-js@4.4.1: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 115f561..da77883 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,4 +7,4 @@ allowBuilds: protobufjs: true sharp: true minimumReleaseAgeExclude: - - '@renderdragonorg/wisp@0.1.1' + - '@renderdragonorg/wisp@0.1.1 || 0.2.0' diff --git a/src/main.tsx b/src/main.tsx index 9ece827..9543fb3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -9,33 +9,15 @@ import '@fontsource/geist-mono'; import '@fontsource/jetbrains-mono'; import wisp, { init } from "@renderdragonorg/wisp"; +import { bindSupabase } from "@renderdragonorg/wisp/supabase"; +import { supabase } from "@/integrations/supabase/client"; const convexUrl = import.meta.env.VITE_CONVEX_URL; const wispSecret = import.meta.env.VITE_WISP_SECRET; if (convexUrl) { - if (wispSecret) { - const endpoint = convexUrl - .replace(".convex.cloud", ".convex.site") - .replace(/\/$/, "") + "/ingest"; - init({ - transport: { - async send(events, opts) { - await fetch(endpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-wisp-token": wispSecret, - }, - body: JSON.stringify({ events }), - keepalive: opts.beacon, - }); - }, - }, - }); - } else { - init({ convexUrl }); - } + init({ convexUrl, wispSecret: wispSecret || undefined }); + bindSupabase(supabase); } // expose for console debugging diff --git a/src/providers/AuthProvider.tsx b/src/providers/AuthProvider.tsx index 42e64ec..ed6240f 100644 --- a/src/providers/AuthProvider.tsx +++ b/src/providers/AuthProvider.tsx @@ -2,44 +2,24 @@ import { useState, useEffect } from "react"; import { User, Session } from "@supabase/supabase-js"; import { supabase } from "@/integrations/supabase/client"; import { AuthContext, AuthResult } from "@/providers/AuthContext"; -import wisp from "@renderdragonorg/wisp"; - -function tryIdentify(userId: string) { - try { wisp.identify(userId); } catch { /* wisp not initialized */ } -} -function tryReset() { - try { wisp.reset(); } catch { /* wisp not initialized */ } -} - export const AuthProvider = ({ children }: { children: React.ReactNode }) => { const [user, setUser] = useState(null); const [session, setSession] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { - // Set up auth state listener FIRST const { data: { subscription }, } = supabase.auth.onAuthStateChange((event, session) => { setSession(session); setUser(session?.user ?? null); setLoading(false); - - if (event === "SIGNED_IN" && session?.user.id) { - tryIdentify(session.user.id); - } else if (event === "SIGNED_OUT") { - tryReset(); - } }); - // THEN check for existing session supabase.auth.getSession().then(({ data: { session } }) => { setSession(session); setUser(session?.user ?? null); setLoading(false); - if (session?.user.id) { - tryIdentify(session.user.id); - } }); return () => subscription.unsubscribe(); diff --git a/supabase.md b/supabase.md new file mode 100644 index 0000000..d554367 --- /dev/null +++ b/supabase.md @@ -0,0 +1,148 @@ +# Supabase Integration + +Wisp comes with optional Supabase integration that: + +1. **Wires auth state** — automatically calls `wisp.identify()` on sign-in and + `wisp.reset()` on sign-out so analytics events are associated with the + authenticated user. +2. **Enriches user profiles** — extracts the user's email, name, and auth + provider (e.g. `github`, `google`, `email`) from the Supabase session and + sends them along so the dashboard can display who the user is. + +--- + +## Quick start + +```ts +import { createClient } from "@supabase/supabase-js"; +import { bindSupabase } from "@renderdragonorg/wisp/supabase"; +import { init } from "@renderdragonorg/wisp"; + +// Initialize Wisp first +init({ convexUrl: "https://your-project.convex.cloud" }); + +// Then bind Supabase — must be called after both are initialized +const supabase = createClient( + import.meta.env.VITE_SUPABASE_URL, + import.meta.env.VITE_SUPABASE_ANON_KEY +); + +const unsubscribe = bindSupabase(supabase); + +// Later, if you need to stop listening: +unsubscribe(); +``` + +That's it. On every `SIGNED_IN` event, Wisp will automatically: + +- Attach the user's Supabase `id` as the Wisp `userId` +- Send the user's email, name (`user_metadata.full_name`), and auth provider + (`app_metadata.provider`) as enriched profile data + +The dashboard's **Machine View** will then display the user's email, name, +and auth provider alongside the device and session data. + +--- + +## Package setup + +`@supabase/supabase-js` is declared as an **optional peer dependency** of +the Wisp SDK. You only need to install it if you plan to use the Supabase +binding: + +```bash +npm install @supabase/supabase-js +``` + +The `bindSupabase` function is exported from the subpath +`@renderdragonorg/wisp/supabase`. + +--- + +## Manual identify (without Supabase) + +If you're using a different auth provider, you can still pass user metadata +by calling `wisp.identify()` directly: + +```ts +import { wisp } from "@renderdragonorg/wisp"; + +wisp.identify("user-abc-123", { + email: "alice@example.com", + name: "Alice Smith", + provider: "google", +}); +``` + +The metadata is forwarded to Convex and stored on the machine record, +making it visible in the dashboard. + +To clear the identity on logout: + +```ts +wisp.reset(); +``` + +--- + +## Data flow + +``` +Supabase auth state change + │ + ▼ +bindSupabase() extracts + • user.id + • user.email + • user.user_metadata.full_name + • user.app_metadata.provider + │ + ▼ +wisp.identify(userId, userInfo) + │ + ▼ +Wisp queues a "session_identify" event +with { email, name, provider, userId } + │ + ▼ +Convex HTTP Action /ingest + │ + ▼ +recordBatchWithGeo() + │ + ▼ +upsertMachine() stores +userEmail, userName, authProvider +on the machines table + │ + ▼ +Dashboard queries expose +the enriched user fields +``` + +--- + +## Dashboard display + +User profile data appears in: + +- **Machine View** — shows Email, Name, Auth Provider, and User ID in a + dedicated "User" section +- **Page Visitors** — shows user email/name in the User column +- **Machine Search** — shows email and name alongside platform and country +- **Sessions Table** — shows the user ID in a User column + +--- + +## Schema + +The `machines` table gains three optional fields: + +| Field | Type | Source | +|----------------|--------|----------------------------| +| `userEmail` | string | `session.user.email` | +| `userName` | string | `user_metadata.full_name` | +| `authProvider` | string | `app_metadata.provider` | + +These are set when a `session_identify` event is processed, and are +preserved across subsequent sessions for the same machine. From 3a522729779b70c6bf2a3c382a68dbb1553ec014 Mon Sep 17 00:00:00 2001 From: coder-soft Date: Sun, 26 Jul 2026 12:19:01 +0500 Subject: [PATCH 3/3] fix: address Greptile security review findings - Scope session_identify enrichment to the originating machine only, preventing cross-machine PII attribution (buildUserInfoMap) - Gate PII fields (userEmail, userName, authProvider) in all dashboard queries behind ctx.auth.getUserIdentity() check - Add pii() helper to consistently null out PII for unauthenticated callers --- convex/dashboard.ts | 26 +++++++++++++++++--------- convex/events.ts | 30 ++++++++++++++++++------------ 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/convex/dashboard.ts b/convex/dashboard.ts index a9bee3a..c045f64 100644 --- a/convex/dashboard.ts +++ b/convex/dashboard.ts @@ -211,9 +211,15 @@ export const getErrorDetails = query({ }); /** Fetch aggregated stats for a single machine. */ +function pii(value: T, identity: unknown): T | null { + return identity ? (value ?? null) : null; +} + export const getMachineStats = query({ args: { machineId: v.string() }, handler: async (ctx, { machineId }) => { + const identity = await ctx.auth.getUserIdentity(); + const machine = await ctx.db .query("machines") .withIndex("by_machineId", (q) => q.eq("machineId", machineId)) @@ -280,9 +286,9 @@ export const getMachineStats = query({ region: machine.region ?? null, city: machine.city ?? null, screen: machine.screen ?? null, - userEmail: machine.userEmail ?? null, - userName: machine.userName ?? null, - authProvider: machine.authProvider ?? null, + userEmail: pii(machine.userEmail, identity), + userName: pii(machine.userName, identity), + authProvider: pii(machine.authProvider, identity), }, stats: { totalSessions, @@ -349,6 +355,7 @@ export const getTopPages = query({ export const getPageVisitors = query({ args: { url: v.string(), startDate: v.string(), endDate: v.string() }, handler: async (ctx, { url, startDate, endDate }) => { + const identity = await ctx.auth.getUserIdentity(); const dayStart = new Date(`${startDate}T00:00:00.000Z`).getTime(); const dayEnd = new Date(`${endDate}T23:59:59.999Z`).getTime(); @@ -384,9 +391,9 @@ export const getPageVisitors = query({ country: machine?.country ?? null, platform: machine?.platform ?? null, userAgent: machine?.userAgent ?? null, - userEmail: machine?.userEmail ?? null, - userName: machine?.userName ?? null, - authProvider: machine?.authProvider ?? null, + userEmail: pii(machine?.userEmail, identity), + userName: pii(machine?.userName, identity), + authProvider: pii(machine?.authProvider, identity), }; }) ); @@ -428,6 +435,7 @@ export const getPageViewsOverTime = query({ export const searchMachines = query({ args: { prefix: v.string() }, handler: async (ctx, { prefix }) => { + const identity = await ctx.auth.getUserIdentity(); if (!prefix || prefix.trim().length === 0) return []; const results = await ctx.db .query("machines") @@ -439,9 +447,9 @@ export const searchMachines = query({ country: m.country ?? null, platform: m.platform ?? null, lastSeenAt: m.lastSeenAt, - userEmail: m.userEmail ?? null, - userName: m.userName ?? null, - authProvider: m.authProvider ?? null, + userEmail: pii(m.userEmail, identity), + userName: pii(m.userName, identity), + authProvider: pii(m.authProvider, identity), })); }, }); diff --git a/convex/events.ts b/convex/events.ts index 8d18307..aa079aa 100644 --- a/convex/events.ts +++ b/convex/events.ts @@ -129,14 +129,20 @@ async function upsertSession( } } -function extractUserInfo(events: Array<{ name: string; payload?: unknown }>): { email?: string; name?: string; provider?: string } | undefined { - const identifyEvent = events.find((e) => e.name === "session_identify"); - if (!identifyEvent?.payload || typeof identifyEvent.payload !== "object") return undefined; - const p = identifyEvent.payload as Record; - const email = typeof p.email === "string" ? p.email : undefined; - const name = typeof p.name === "string" ? p.name : undefined; - const provider = typeof p.provider === "string" ? p.provider : undefined; - return email || name || provider ? { email, name, provider } : undefined; +function buildUserInfoMap(events: Array<{ machineId: string; name: string; payload?: unknown }>): Map { + const map = new Map(); + for (const event of events) { + if (event.name !== "session_identify") continue; + if (!event.payload || typeof event.payload !== "object") continue; + const p = event.payload as Record; + const email = typeof p.email === "string" ? p.email : undefined; + const name = typeof p.name === "string" ? p.name : undefined; + const provider = typeof p.provider === "string" ? p.provider : undefined; + if (email || name || provider) { + map.set(event.machineId, { email, name, provider }); + } + } + return map; } export const recordBatch = mutation({ @@ -144,7 +150,7 @@ export const recordBatch = mutation({ handler: async (ctx, { events }) => { // Process oldest-first so ordering-dependent bookkeeping (session creation, counters) is correct. const sorted = [...events].sort((a, b) => a.timestamp - b.timestamp); - const userInfo = extractUserInfo(sorted); + const userInfoMap = buildUserInfoMap(sorted); for (const event of sorted) { const isBookkeeping = event.name === "session_start" || event.name === "session_identify"; @@ -154,7 +160,7 @@ export const recordBatch = mutation({ userId: event.userId, timestamp: event.timestamp, meta: isBookkeeping ? event.payload : undefined, - userInfo, + userInfo: userInfoMap.get(event.machineId), }); await upsertSession(ctx, { @@ -230,7 +236,7 @@ export const recordBatchWithGeo = internalMutation({ }, handler: async (ctx, { events, geo }) => { const sorted = [...events].sort((a, b) => a.timestamp - b.timestamp); - const userInfo = extractUserInfo(sorted); + const userInfoMap = buildUserInfoMap(sorted); for (const event of sorted) { const isBookkeeping = event.name === "session_start" || event.name === "session_identify"; @@ -241,7 +247,7 @@ export const recordBatchWithGeo = internalMutation({ timestamp: event.timestamp, meta: isBookkeeping ? event.payload : undefined, geo, - userInfo, + userInfo: userInfoMap.get(event.machineId), }); await upsertSession(ctx, {